Proxy any class by interface in C# with DispatchProxy

nullabletype

Steven Davies

Posted on October 22, 2021

Proxy any class by interface in C# with DispatchProxy

I was recently tasked with figuring out a way to performance test database calls being made via NHibernate to PostgreSQL. As far as I could find, NHibernate doesn't expose any really easy ways to intercept and manually log both the SQL and execution times for each query.

After a little more digging, I came across a handy lesser-known tool that allows you to easily proxy any interface, be it user-defined or from a third party library. This tool lives in the System.Reflection.DispatchProxy nuget package. With a little code, you can wrap any instance of a class that implements an interface with a proxy to not only log but also manipulate both arguments and returned data.

Example

Take this simple class and interface.

interface IHello
{
    bool SayHello(string name);
}

class Hello : IHello
{
    public bool SayHello(string name)
    {
        Console.WriteLine($"Hello {name}");
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nothing special, only one method exposed which takes a simple name parameter, writes out a string to the console and returns a bool indicating success.

Say, although contrived in this example, we wanted to record calls going to SayHello.

class HelloDispatchProxy<T> : DispatchProxy where T : class, IHello
{
    private IHello Target { get; set; }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        // Code here to track time, log call etc.
        var result = targetMethod.Invoke(Target, args);
        return result;
    }

    public static T CreateProxy(T target)
    {
        var proxy = Create<T, HelloDispatchProxy<T>>() as HelloDispatchProxy<T>;
        proxy.Target = target;
        return proxy as T;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is our DispatchProxy, HelloDispatchProxy. Usage is pretty simple.

IHello hello = HelloDispatchProxy<IHello>.CreateProxy(new Hello());
Enter fullscreen mode Exit fullscreen mode

The CreateProxy method calls Create<T> on the base abstract class to generate a proxy object for you. The target instance is then assigned to the Target property before returning the proxy cast to type T (IHello in this instance).

Any method that is then called on the proxied IHello instance will trigger Invoke(). It's your proxies responsibility to call the target method, pass through the parameters and return the result. In this method, you can do whatever you like, and you'll also have access to your original Target instance.

You can see a complete example of this over on my GitHub

With great power comes great responsibility

This allows you quite a lot of control, so be careful what you do with args and the returned result or you can cause some unexpected behaviour.

Performance testing

DispatchProxy lives under the System.Reflection namespace. Because of this, I was curious about what overhead is introduced by this approach. What better way to understand than to test it myself.

I set up a slightly over-engineered set of tests to give a raw comparison between calls to both an unproxied and proxied instance. I ran these multiple times to get an average before analysing the results.

    100000 W/O  100000 W    % Increase
    2233.023    2382.8835   6.71%
    2235.5306   2398.1216   7.27%
    2236.2449   2386.4416   6.72%
    2236.8529   2389.209    6.81%
    2239.9692   2402.4627   7.25%
    2227.0108   2367.0306   6.29%
    2237.8432   2384.8429   6.57%
    2233.5528   2392.8394   7.13%
    2238.411    2398.3124   7.14%
    2235.7398   2388.134    6.82%
    2262.3454   2380.0595   5.20%
    2340.0243   2385.0553   1.92%
    2249.3693   2383.6925   5.97%
    2246.9835   2381.507    5.99%
    2240.7711   2381.4675   6.28%
    2232.5296   2374.0465   6.34%
    2248.0095   2359.8636   4.98%
    2250.2518   2394.8422   6.43%
    2234.7757   2387.8114   6.85%
    2238.7645   2387.8047   6.66%


    0.00112245  0.001192661 6.27%
Enter fullscreen mode Exit fullscreen mode

So on average, the execution took an extra 6 or so percent with just the introduction of the proxy. Bear in mind in this current state, the proxy is doing nothing useful at all, but this helps put into context the efficiency.

But if you were to add something slow like a blocking console write line in the proxy, the % increase shoots up from 6.27% to over 100%.

System.Console.WriteLine($"Going to call {targetMethod.Name}");
Enter fullscreen mode Exit fullscreen mode

The most important part with the performance impact is what you decide to do in the Invoke method, something you'll need to benchmark yourself.

In all but the highest performance-critical applications, the overhead of the proxy itself is minor. If you take for example proxying 2-3 DB calls in the context of something like a HTTP web request or message handler the impact is negligible. In fact, in my testing with a real-world application, the numbers are within an acceptable margin of error even when tracing into an APM tool.

NHibernate & PostgreSQL

If you're specifically interested in collecting information from NHibernate as I was, you can do this by wrapping the IDBCommand in a custom driver implementation.

public class ProfiledNpgsqlDriver : NpgsqlDriver {
    public override IDbCommand CreateCommand() {
        var command = base.CreateCommand();

        if (ConfigurationManager.AppSettings["EnableDBTracing"]?.ToLower() == "true") {
            command = ProfiledDbCommandDispatchProxy<IDbCommand>.CreateProxy(command);
        }

        return command;
    }
}
Enter fullscreen mode Exit fullscreen mode

You can access the command being executed from the target IDBCommand's Command property and record time etc. with a StopWatch. I went a step further and implemented the proxy as an IObservable<T> so I can handle it differently depending on the use case.

Wrap up

Hopefully, you found this interesting. Can you think of any good use cases for this, or have any comments? Would love to hear them ❤

💖 💪 🙅 🚩
nullabletype
Steven Davies

Posted on October 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related