Interception

September 19, 2024 ยท View on GitHub

Decorator with Castle DynamicProxy

Decorator pattern is a good fit for implementing cross-cutting concerns.

But we can extend it further to implement AOP Interception with help of Castle DynamicProxy.

Let's define an extension method for intercepting interfaces and classes:

usings ...
namespace DryIoc.Docs;
using DryIoc;
using DryIoc.ImTools;
using Castle.DynamicProxy;
using IInterceptor = Castle.DynamicProxy.IInterceptor;
using LinFu.DynamicProxy;

using NUnit.Framework;

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;


public static class DryIocInterception
{
    private static readonly DefaultProxyBuilder _proxyBuilder = new DefaultProxyBuilder();

    public static void Intercept<TService, TInterceptor>(this IRegistrator registrator, object serviceKey = null)
        where TInterceptor : class, IInterceptor
    {
        var serviceType = typeof(TService);

        Type proxyType;
        if (serviceType.IsInterface)
            proxyType = _proxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(
                serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
        else if (serviceType.IsClass)
            proxyType = _proxyBuilder.CreateClassProxyTypeWithTarget(
                serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
        else
            throw new ArgumentException(
                $"Intercepted service type {serviceType} is not a supported, cause it is nor a class nor an interface");

        registrator.Register(serviceType, proxyType,
            made: Made.Of(pt => pt.PublicConstructors().FindFirst(ctor => ctor.GetParameters().Length != 0),
                Parameters.Of.Type<IInterceptor[]>(typeof(TInterceptor[]))),
            setup: Setup.DecoratorOf(useDecorateeReuse: true, decorateeServiceKey: serviceKey));
    }
} 

Now define a method interceptor:

public class FooLoggingInterceptor : IInterceptor
{
    public List<string> LogLines = new List<string>();
    private void Log(string line) => LogLines.Add(line);

    public void Intercept(IInvocation invocation)
    {
        Log($"Invoking method: {invocation.GetConcreteMethod().Name}");
        invocation.Proceed();
    }
}

Register service and its interceptor as a normal services, then link them together via Intercept method:


public class Register_and_use_interceptor
{
    public interface IFoo
    {
        void Greet();
    }

    public class Foo : IFoo
    {
        public void Greet() { }
    }

    [Test]
    public void Example()
    {
        var container = new Container();
        container.Register<IFoo, Foo>();
        container.Register<FooLoggingInterceptor>(Reuse.Singleton);
        container.Intercept<IFoo, FooLoggingInterceptor>();

        var foo = container.Resolve<IFoo>();
        foo.Greet();

        // examine that logging indeed was hooked up
        var logger = container.Resolve<FooLoggingInterceptor>();
        Assert.AreEqual("Invoking method: Greet", logger.LogLines[0]);
    }
}

Async Interceptor with Castle.DynamicProxy

The Castle.Core.AsyncInterceptor package provides the IAsyncInterceptor and AsyncDeterminationInterceptor class which we may use to implement the interception of async methods.


public class AsyncInterceptor<T> : AsyncDeterminationInterceptor where T : IAsyncInterceptor
{
    public AsyncInterceptor(T asyncInterceptor) : base(asyncInterceptor) { }
}

public static class DryIocInterceptionAsync
{
    public static void InterceptAsync<TService, TInterceptor>(this IRegistrator registrator, object serviceKey = null)
        where TInterceptor : class, IAsyncInterceptor
    {
        registrator.Register<AsyncInterceptor<TInterceptor>>();
        registrator.Intercept<TService, AsyncInterceptor<TInterceptor>>(serviceKey);
    }
}

And the usage example:


public class Register_and_use_async_interceptor
{
    public interface IFoo
    {
        Task<string> HeyAsync(string name);
    }

    public class Foo : IFoo
    {
        public async Task<string> HeyAsync(string name)
        {
            await Task.Delay(30);
            return $"Hey {name} async!";
        }
    }

    public class SomeAsyncInterceptor : ProcessingAsyncInterceptor<string>
    {
        public List<string> Interceptions = new List<string>();

        protected override string StartingInvocation(IInvocation invocation)
        {
            var method = invocation.GetConcreteMethod().Name;
            var arg = invocation.Arguments[0];
            return $"intercept BEFORE async method call of `{method}` with argument `{arg}`; ";
        }

        protected override void CompletedInvocation(IInvocation invocation, string state)
        {
            var method = invocation.GetConcreteMethod().Name;
            var res = invocation.ReturnValue;
            Interceptions.Add(state + "intercept AFTER async method call of `{methodName}` with the result `{res}`;");
        }
    }

    [Test]
    public async Task Example()
    {
        var container = new Container();

        container.Register<IFoo, Foo>();
        container.Register<SomeAsyncInterceptor>(Reuse.Singleton);
        container.InterceptAsync<IFoo, SomeAsyncInterceptor>();

        var foo = container.Resolve<IFoo>();

        var result = await foo.HeyAsync("NyanCat");

        var interceptor = container.Resolve<SomeAsyncInterceptor>();
        Assert.AreEqual(1, interceptor.Interceptions.Count);
    }
}

Decorator with LinFu DynamicProxy

Lately, there was a new release of LinFu.DynamicProxy with .NET Standard 2.0 support.

It is interesting to at least have an alternative to Castle.DynamicProxy. Let's try it out :-)

public static class DryIocInterceptionLinFu
{
    private static readonly ProxyFactory _proxyFactory = new ProxyFactory();

    private static readonly MethodInfo _createProxyMethod = typeof(ProxyFactory)
        .Method(nameof(ProxyFactory.CreateProxy), typeof(IInvokeWrapper), typeof(Type[]));

    public static void InterceptInvocation<TService, TInvokeWrapper>(this IRegistrator registrator, object serviceKey = null)
        where TInvokeWrapper : class, IInvokeWrapper
    {
        var serviceType = typeof(TService);
        if (!serviceType.IsAbstract)
            throw new ArgumentException($"Non-abstract {serviceType} are not a supported.");

        var createProxyMethod = _createProxyMethod.MakeGenericMethod(serviceType);

        registrator.Register(serviceType,
            made: Made.Of(FactoryMethod.Of(createProxyMethod, _proxyFactory),
                Parameters.Of.Type<IInvokeWrapper>(typeof(TInvokeWrapper)).Type<Type[]>(_ => null)),
            setup: Setup.DecoratorOf(useDecorateeReuse: true, decorateeServiceKey: serviceKey));
    }
}

// Simplify implementation of wrapper
public abstract class InvokeWrapperBase<TIntercepted> : IInvokeWrapper
{
    public readonly TIntercepted Target;
    protected InvokeWrapperBase(TIntercepted target)
    {
        Target = target;
    }
    public virtual void BeforeInvoke(InvocationInfo info) { }
    public virtual void AfterInvoke(InvocationInfo info, object returnValue) { }
    public virtual object DoInvoke(InvocationInfo info) => info.TargetMethod.Invoke(Target, info.Arguments);
}

Implement IInvokeWrapper to intercept the methods of IBar instances. Then register service, BarInvokeWrapper, and link them together via Intercept method:


public class Register_and_use_interceptor_with_LinFu
{
    public interface IBar
    {
        void Greet();
    }
    public class Bar : IBar
    {
        public void Greet() { }
    }

    public class BarLogger
    {
        public List<string> LogLines = new List<string>();
        public void Log(string line) => LogLines.Add(line);
    }

    public class BarInvokeWrapper : InvokeWrapperBase<IBar>
    {
        private readonly BarLogger _logger;
        public BarInvokeWrapper(IBar bar, BarLogger logger) : base(bar)
        {
            _logger = logger;
        }

        public override object DoInvoke(InvocationInfo info)
        {
            _logger.Log($"Invoking method: {info.TargetMethod.Name}");
            // may be optimized, because we know the actual `Target` object here.
            //if (info.TargetMethod.Name == nameof(IBar.Greet))
            //    Target.Greet();
            return base.DoInvoke(info);
        }
    }

    [Test]
    public void Example()
    {
        var container = new Container();

        container.Register<IBar, Bar>();
        container.Register<BarLogger>(Reuse.Singleton);
        container.Register<BarInvokeWrapper>();
        container.InterceptInvocation<IBar, BarInvokeWrapper>();

        var foo = container.Resolve<IBar>();
        foo.Greet();

        // examine that logging indeed was hooked up
        var logger = container.Resolve<BarLogger>();
        Assert.AreEqual("Invoking method: Greet", logger.LogLines[0]);
    }
}