Skip to main content

Using Castle DynamicProxy to create... dynamic proxy!

The subject of this post is not really new I guess, but since I myself stumbled upon such puzzle, I decided to blog about my own experience.

Let's say that you have some project with a back-end accessible through WCF-service(s) facade, and a client app written in C#. Let me add to this that you also have some business logic encapsulated in a form of a special service class that implements special interface. Moreover, you share that interface across your applications.
public interface IUserService
{
    AuthenticatedUser Authenticate(string login, string password);

    void ChangeUserOffice(int userId, int officeId);
}

[Export(typeof(IUserService))]
public class BackendUserService : IUserService
{
    public AuthenticatedUser Authenticate(string login, string password)
    {
        // user authentication logic
    }

    public void ChangeUserOffice(int userId, int officeId)
    {
        // office change logic
    }
}
That's basically what you have on your back-end: service with business logic exposed to clients through WCF facade. Let's go to the client.
[Export(typeof(IUserService))]
public class ClientUserService : IUserService
{
    public AuthenticatedUser Authenticate(string login, string password)
    {
        // call WCF facade
        using (var serviceClient = new UserServiceClient())
        {
            return serviceClient.AuthenticateUser(login, password);
        }
    }

    public void ChangeUserOffice(int userId, int officeId)
    {
        // you don't need this method on a client, you've simply left it not implemented
        throw new NotImplementedException();
    }
}
N.B. I'm using MEF 2 (System.Compostion.*) as a DI container, that's why I added Export attribute for both IUserService implementations. This detail will appear to be quite important later.

This not implemented ChangeUserOffice method doesn't look neat, does it?

Recently, I had the same, even worse situation - interface was much richer, with around 10 methods, and only 3 of them were used on a client.
My first try to tackle with this problem involved generating (using T4 template) special dummy class that consisted of virtual not implemented methods. This class was further inherited by client's service and selected methods were overridden.
At first sight, issue was solved. However, further experience proved that developers constantly forget about regenerating T4 template, after changing service's interface, which leads to compile-time errors (broken CI builds, anger, frustration, etc.).
The situation was no good, so I decided to implement different approach based on dynamic method dispatching. Castle DynamicProxy looked like an ideal library for that purpose.
First of all I needed some base class to do the basic dispatching work. After some time I've come up with this one:
namespace DynamicServiceProxy
{
    public class Proxy<TServiceInterface> : IInterceptor
    {
        public Proxy()
        {
            var generator = new ProxyGenerator();
            this.Interface =
                (TServiceInterface) generator.CreateInterfaceProxyWithoutTarget(typeof (TServiceInterface), this);
        }

        public TServiceInterface Interface { get; private set; }

        void IInterceptor.Intercept(IInvocation invocation)
        {
            var proxyType = this.GetType();
            var argTypes = this.GetArgumentsTypes(invocation.Arguments);

            var method = proxyType.GetMethod(
                invocation.Method.Name, BindingFlags.Public | BindingFlags.Instance,
                null, argTypes, null);

            if (method != null)
            {
                invocation.ReturnValue = method.Invoke(this, invocation.Arguments);
                return;
            }

            // No match was found for required method
            throw new NotImplementedException(
                $"No match was found for method {invocation.Method.Name}.");
        }

        private Type[] GetArgumentsTypes(object[] args)
        {
            var argTypes = new Type[args.GetLength(0)];

            var index = 0;
            foreach (var arg in args)
            {
                argTypes[index] = arg.GetType();
                index++;
            }

            return argTypes;
        }
    }
}
Interface property here is an actual interceptor, that I will expose to a proxy users. As you can see, from IInterceptor.Intercept method implementation, it will process every method invocation and, if respective method is present in concrete derived class, invoke it. Otherwise, NotImplementedException will be thrown.
Alright, we have the base class, now it's time to use it. Let's look at new ClientUserService class:
public class ClientUserService : Proxy<IUserService>
{
    public AuthenticatedUser Authenticate(string login, string password)
    {
        // call WCF facade
        using (var serviceClient = new UserServiceClient())
        {
            return serviceClient.AuthenticateUser(login, password);
        }
    }
}
That's it - ready for use proxy, that use DynamicProxy technology under the hood.
However, if you noticed, I'm no longer using Export attribute. That's because MEF will throw an exception, if export this class straightforward, using just attribute. To overcome this, I will need one more thing - using MEF's special ConventionBuilder class, I can register proxy's Interface property as an export:
public void UpdateConvetion(ConventionBuilder builder)
{
    builder.ForType<ClientUserService>()
        .ExportProperty<IUserService>(
            x => x.Interface,
            b => b.AsContractType<IUserService>());
}
Done. Now I can import my proxy just by adding Import attribute to any appropriate property.

To summarize, let me add that I made a transition from compile-time solution to a runtime one, with all possible drawbacks - no compile-time checks for conformity with interface declarations. To some extent, this check can be replaced by special unit-test - I will show how to do such testing in my next post. Thank you and stay tuned.

UPD: Code for this and next article is on GitHub.

Comments

Popular posts from this blog