Click here to Skip to main content
15,881,757 members
Articles / Programming Languages / C#

Castle Dynamic Proxy Interceptors to Track Model Changes and Trigger Rules

Rate me:
Please Sign up or sign in to vote.
5.00/5 (10 votes)
27 Jun 2023CPOL7 min read 21.2K   8   6
Add ability to track changes in a model class; use a proxy interceptor to execute a rule attached to a model property
This article discusses the Castle opensource library that can be useful for dynamic proxy generation and interception of calls to the proxy methods and properties.

Introduction

Good day, and welcome to my new article.

I would like to present to you a powerful opensource library called Castle DynamicProxy that gives you the ability to intercept calls to your model classes using proxies. The proxies are generated dynamically at runtime, so you don't need to change your model classes to start intercepting their properties or methods.

By the way, it is not a good design decision to have methods and any other logic in model classes.

Let me start by defining a user story.

User Story #3: Interception of Model Calls

  • Add the ability to track changes in a model class
  • Store a sequence of calls in a collection attached to the model class

Implementation - Model

Let's create a new Visual Studio project, but this time let's use the .NET Core class library and check how our code works using the xUnit testing framework. We add a new main project and name it DemoCastleProxy:

Image 1

When the main project is created, add a new xUnit project DemoCastleProxyTests to the solution, we will need it to check how our proxies demo works:

Image 2

Our user story says that we need to have a collection for change tracking inside the model class, so we start from an interface that defines this collection. If we want to create more model classes, we can reuse this interface. Let’s add a new interface to the main project:

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace DemoCastleProxy
{
    public interface IModel
    {
        List<string> PropertyChangeList { get; }
    }
}

Now we can add the model class:

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace DemoCastleProxy
{
    public class PersonModel : IModel
    {
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual DateTime? BirthDate { get; set; }

        public virtual List<string> PropertyChangeList { get; set; } = 
            new List<string>();
    }
}

As you can see, PersonModel implements interface IModel and PropertyChangeList is initialized every time we create an instance of PersonModel. You can also see that I marked all the properties using the virtual keyword. This is an important part of the model definition.

Castle DynamicProxy can only intercept virtual properties, using polymorphism to achieve that. Actually, the Castle proxy engine works in such a way by creating inherited classes from your model classes and it overrides all virtual properties. When you call the overridden property, it executes an interceptor first, and only then it hands over your call to a base model class.

You can try to do this yourself by creating a proxy manually. It may look like this:

C#
public class PersonModelProxy : PersonModel
{
    public override string FirstName
    {
        get
        {
            Intercept("get_FirstName", base.FirstName);
            return base.FirstName;
        }

        set
        {
            Intercept("set_FirstName", value);
            base.FirstName = value;
        }
    }

    private void Intercept(string propertyName, object value)
    {
        // do something here
    }
}

but Castle does it for us at runtime, in a generic way and proxy classes will have the same properties as the original model classes - thus we will only need to maintain our model classes.

Implementation - Proxy Factory

Many of you know that Proxy is a structural design pattern, and I always recommend to developers to read about OOP design and especially read the Gang of Four Design Patterns book.

I will use another design pattern, Factory Method, to implement the generic logic for a proxy generation.

But before all that, we need to add Castle.Core NuGet package to the main project:

Image 3

Now, I will start from an interface:

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace DemoCastleProxy
{
    public interface IProxyFactory
    {
        T GetModelProxy<T>(T source) where T : class;
    }
}

and will add its implementation:

C#
using Castle.DynamicProxy;
using System;

namespace DemoCastleProxy
{
    public class ProxyFactory : IProxyFactory
    {
        private readonly IProxyGenerator _proxyGenerator;
        private readonly IInterceptor _interceptor;

        public ProxyFactory(IProxyGenerator proxyGenerator, IInterceptor interceptor)
        {
            _proxyGenerator = proxyGenerator;
            _interceptor = interceptor;
        }

        public T GetModelProxy<T>(T source) where T : class
        {
            var proxy = _proxyGenerator.CreateClassProxyWithTarget(source.GetType(), 
                        source, new IInterceptor[] { _interceptor }) as T;
            return proxy;
        }
    }
}

Using the interface gives us the flexibility to have several implementations of IProxyFactory and choose one of them in the Dependency Injection registration at startup.

We use the CreateClassProxyWithTarget method from the Castle framework to create a proxy object from the supplied model object.

Now we need to implement an interceptor that will be passed to the ProxyFactory constructor and supplied to the CreateClassProxyWithTarget method as a second parameter.

The interceptor code will be:

C#
using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace DemoCastleProxy
{
    public class ModelInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            invocation.Proceed();

            var method = invocation.Method.Name;

            if (method.StartsWith("set_"))
            {
                var field = method.Replace("set_", "");
                var proxy = invocation.Proxy as IModel;

                if (proxy != null)
                {
                    proxy.PropertyChangeList.Add(field);
                }
            }
        }
    }
}

Each call to a proxy object will execute the Intercept method. In this method, we check that if a property setter is called, then we add the name of the called property to PropertyChangeList.

Now we can compile our code.

Implementation - Unit Tests

We need to run our code to make sure that it works, and one of the possible ways is to create a unit test. It will be much faster than the creation of an application that will use our proxies.

Here at Pro Coders, we pay close attention to unit testing, because unit tested code will work in an application too. Furthermore, if you refactor code covered by unit tests, you can be assured that after the refactoring, your code will work correctly if unit tests pass.

Let's add the first test:

C#
using Castle.DynamicProxy;
using DemoCastleProxy;
using System;
using Xunit;

namespace DemoCastleProxyTests
{
    public class DemoTests
    {
        private IProxyFactory _factory;

        public DemoTests()
        {
            _factory = new ProxyFactory(new ProxyGenerator(), new ModelInterceptor());
        }

        [Fact]
        public void ModelChangesInterceptedTest()
        {
            PersonModel model = new PersonModel();
            PersonModel proxy = _factory.GetModelProxy(model);
            proxy.FirstName = "John";

            Assert.Single(model.PropertyChangeList);
            Assert.Single(proxy.PropertyChangeList);
            Assert.Equal("FirstName", model.PropertyChangeList[0]);
            Assert.Equal("FirstName", proxy.PropertyChangeList[0]);
        }
    }
}

In xUnit, we need to mark each test method with a [Fact] attribute.

In the DemoTests constructor, I created _factory and supplied a new instance of ModelInterceptor as a parameter, though in an application we will use Dependency Injection for ProxyFactory instantiation.

Now, in every method of our test class, we can use _factory to create proxy objects.

My test simply creates a new model object, then it generates a proxy object from the model. Now any calls to the proxy object should be intercepted and PropertyChangeList will be filled.

To run your unit test, put your cursor to any part of the test method body and click [Ctrl+R]+[Ctrl+T]. If hotkey doesn't work, use the context menu or Test Explorer window.

If you put a breakpoint, you can see the values of the variables we used:

Image 4

As you can see, we changed the FirstName property and it has appeared in the PropertyChangeList.

Implementation - Rule Engine

Let's make this exercise more interesting and use our interceptor to execute a rule attached to a model property.

We will use a C# attribute to attach a type of rule that the interceptor should execute, let's create it:

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace DemoCastleProxy
{
    public class ModelRuleAttribute : Attribute
    {
        public Type Rule { get; private set; }

        public ModelRuleAttribute(Type rule)
        {
            Rule = rule;
        }
    }
}

Having the name of the property allows the interceptor to use reflection to read attributes attached to the property and execute the rule.

To make it elegant, we will define the IModelRule interface:

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace DemoCastleProxy
{
    public interface IModelRule
    {
        void Execute(object model, string fieldName);
    }
}

and our rule will implement it, like so:

C#
using System;
using System.Collections.Generic;
using System.Text;

namespace DemoCastleProxy
{
    public class PersonRule : IModelRule
    {
        public void Execute(object model, string fieldName)
        {
            var personModel = model as PersonModel;

            if (personModel != null && fieldName == "LastName")
            {
                if (personModel.FirstName?.ToLower() == "john" &&
                    personModel.LastName?.ToLower() == "lennon")
                {
                    personModel.BirthDate = new DateTime(1940, 10, 9);
                }
            }
        }
    }
}

The rule will check if the changed field is LastName (only when the LastName setter is executed) and if FirstName and LastName have the values "John Lennon" in the lower or upper case, then it will set the BirthDate field automatically.

Now we need to attach the rule to our model:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;

namespace DemoCastleProxy
{
    public class PersonModel : IModel
    {
        public virtual string FirstName { get; set; }

        [ModelRule(typeof(PersonRule))]
        public virtual string LastName { get; set; }

        public virtual DateTime? BirthDate { get; set; }

        public virtual List<string> PropertyChangeList { get; set; } =
            new List<string>();
    }
}

You can see the [ModelRule(typeof(PersonRule))] attribute added above the LastName property and we supplied to .NET the type of the rule.

We also need to modify ModelInterceptor adding the functionality to execute rules, the new code added after the // rule execution comment:

C#
using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace DemoCastleProxy
{
    public class ModelInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            invocation.Proceed();

            var method = invocation.Method.Name;

            if (method.StartsWith("set_"))
            {
                var field = method.Replace("set_", "");

                var proxy = invocation.Proxy as IModel;

                if (proxy != null)
                {
                    proxy.PropertyChangeList.Add(field);
                }

                // rule execution
                var model = ProxyUtil.GetUnproxiedInstance(proxy) as IModel;

                var ruleAttribute = model.GetType().GetProperty(field).GetCustomAttribute
                                    (typeof(ModelRuleAttribute)) as ModelRuleAttribute;

                if (ruleAttribute != null)
                {
                    var rule = Activator.CreateInstance(ruleAttribute.Rule) as IModelRule; 

                    if (rule != null)
                    {
                        rule.Execute(invocation.Proxy, field);
                    }
                }
            }
        }
    }
}

The interceptor simply reads custom attributes of the fired property using reflection, and if it finds a rule attached to the property, it creates the rule instance and executes it.

The last bit now is to check that our rule engine works, let's create another unit test for that in our DemoTests class:

C#
[Fact]
public void ModelRuleExecutedTest()
{
    var model = new PersonModel();
    var proxy = _factory.GetModelProxy(model);
    proxy.FirstName = "John";
    Assert.NotEqual("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));

    proxy.LastName = "Lennon";
    Assert.Equal("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));
}

This test sets FirstName to "John" and checks that the BirthDate property is not 1940-10-09, then it sets LastName to "Lennon" and checks that the BirthDate is 1940-10-09 now.

We can run it and make sure that the interceptor executed the rule and changed the BirthDate value. We also can use the debugger to see what is happening when we set the LastName property, I know for sure - it is interesting.

Also, it is a good practice to have negative tests as well - which test the opposite scenarios. Let's create a test that will check that nothing happens if the full name is not "John Lennon":

C#
[Fact]
public void ModelRuleNotExecutedTest()
{
    var model = new PersonModel();
    var proxy = _factory.GetModelProxy(model);
    proxy.FirstName = "John";
    Assert.NotEqual("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));

    proxy.LastName = "Travolta";
    Assert.NotEqual("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));
}

You can find the full solution code on my GitHub, folder DemoCastleProxy-story3:

Upgrading code to .NET 7.0 and Castle.Core 5.1.1

Having a few questions from readers I upgraded this article codebase to the latest version of .NET and Castle. As I see it works as expected and I made a screenshot:

Image 5

If you look at the bottom, I highlighted the real type of the proxy object - it is "Castle.Proxies.PersonMoldeProxy".

Summary

Today, we discussed the Castle open-source library that can be useful for dynamic proxy generation and interception of calls to the proxy methods and properties.

Because proxy is an extension of an original class, you can substitute the model objects with proxy objects, for example, when your Data access layer reads data from a database and returns back proxy objects (instead of original objects) to the caller, and then you will be able to track all the changes that happened with the returned proxy objects.

We also considered using unit tests to check that the created classes work as expected and to debug our code.

Thank you for reading!

History

  • 24th October, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Pro Coders
Australia Australia
Programming enthusiast and the best practices follower

Comments and Discussions

 
GeneralNot so Pin
Niemand252-Jul-23 22:11
professionalNiemand252-Jul-23 22:11 
GeneralRe: Not so Pin
Ev Uklad2-Jul-23 22:23
professionalEv Uklad2-Jul-23 22:23 
QuestionIt does not work for Castle.Core 5.1.1 base on .net core 6 Pin
tomyjc12325-Jun-23 18:20
tomyjc12325-Jun-23 18:20 
AnswerRe: It does not work for Castle.Core 5.1.1 base on .net core 6 Pin
OriginalGriff25-Jun-23 18:21
mveOriginalGriff25-Jun-23 18:21 
AnswerRe: It does not work for Castle.Core 5.1.1 base on .net core 6 Pin
Ev Uklad25-Jun-23 20:29
professionalEv Uklad25-Jun-23 20:29 
AnswerRe: It does not work for Castle.Core 5.1.1 base on .net core 6 Pin
Ev Uklad27-Jun-23 13:54
professionalEv Uklad27-Jun-23 13:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.