3

I have many AOP libraries that use Castle DynamicProxy with Autofac DI container for logging, auditing, transaction control, etc.

I wonder if there is a way to declare interceptors using the default .NET Core DI container. It will be good to have this flexibility since many .NET Core projects don't use Autofac.

Rodrigo
  • 53
  • 1
  • 6

2 Answers2

12

Yes, you can use DynamicProxy using Core DI. I've written up a blog post explaining it at http://codethug.com/2021/03/17/Caching-with-Attributes-in-DotNet-Core5/, but here is the code for it:

Create an attribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CacheAttribute : Attribute
{
    public int Seconds { get; set; } = 30;
}

Create an interceptor (requires Castle.Core nuget package)

public class CacheInterceptor : IInterceptor
{
    private IMemoryCache _memoryCache;
    public CacheInterceptor(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    // Create a cache key using the name of the method and the values
    // of its arguments so that if the same method is called with the
    // same arguments in the future, we can find out if the results 
    // are cached or not
    private static string GenerateCacheKey(string name, 
        object[] arguments)
    {
        if (arguments == null || arguments.Length == 0)
            return name;
        return name + "--" + 
            string.Join("--", arguments.Select(a => 
                a == null ? "**NULL**" : a.ToString()).ToArray());
    }

    public void Intercept(IInvocation invocation)
    {
        var cacheAttribute = invocation.MethodInvocationTarget
            .GetCustomAttributes(typeof(CacheAttribute), false)
            .FirstOrDefault() as CacheAttribute;

        // If the cache attribute is added ot this method, we 
        // need to intercept this call
        if (cacheAttribute != null)
        {
            var cacheKey = GenerateCacheKey(invocation.Method.Name, 
                invocation.Arguments);
            if (_memoryCache.TryGetValue(cacheKey, out object value))
            {
                // The results were already in the cache so return 
                // them from the cache instead of calling the 
                // underlying method
                invocation.ReturnValue = value;
            }
            else
            {
                // Get the result the hard way by calling 
                // the underlying method
                invocation.Proceed();
                // Save the result in the cache
                var options = new MemoryCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = 
                        new System.TimeSpan(hours: 0, minutes: 0, 
                            seconds: cacheAttribute.Seconds)
                };
                _memoryCache.Set(cacheKey, invocation.ReturnValue, 
                    options);
            }
        }
        else
        {
            // We don't need to cache the results, 
            // nothing to see here
            invocation.Proceed();
        }
    }
}

Add an extension method to help register classes in DI:

public static void AddProxiedScoped<TInterface, TImplementation>
    (this IServiceCollection services)
    where TInterface : class
    where TImplementation : class, TInterface
{
    // This registers the underlying class
    services.AddScoped<TImplementation>();
    services.AddScoped(typeof(TInterface), serviceProvider =>
    {
        // Get an instance of the Castle Proxy Generator
        var proxyGenerator = serviceProvider
            .GetRequiredService<ProxyGenerator>();
        // Have DI build out an instance of the class that has methods
        // you want to cache (this is a normal instance of that class 
        // without caching added)
        var actual = serviceProvider
            .GetRequiredService<TImplementation>();
        // Find all of the interceptors that have been registered, 
        // including our caching interceptor.  (you might later add a 
        // logging interceptor, etc.)
        var interceptors = serviceProvider
            .GetServices<IInterceptor>().ToArray();
        // Have Castle Proxy build out a proxy object that implements 
        // your interface, but adds a caching layer on top of the
        // actual implementation of the class.  This proxy object is
        // what will then get injected into the class that has a 
        // dependency on TInterface
        return proxyGenerator.CreateInterfaceProxyWithTarget(
            typeof(TInterface), actual, interceptors);
    });
}

Add these lines to ConfigureServices in Startup.cs

// Setup Interception
services.AddSingleton(new ProxyGenerator());
services.AddScoped<IInterceptor, CacheInterceptor>(

After that, if you want to use the cache interceptor, you need to do two things:

First, add the attribute to your method

[Cache(Seconds = 30)]
public async Task<IEnumerable<Person>> GetPeopleByLastName(string lastName)
{
    return SomeLongRunningProcess(lastName);
}

Second, register the class in DI using the Proxy/Interception:

services.AddProxiedScoped<IPersonRepository, PersonRepository>();

Instead of the normal way without the Proxy/Interception:

services.AddScoped<IPersonRepository, PersonRepository>();
CodeThug
  • 3,054
  • 1
  • 21
  • 17
5

The base .NET Core container does not have any extra features like interceptors. The whole reason the DI container in .NET Core can be swapped out for something like Autofac is so you can move to a different container once you outgrow the default one.

Travis Illig
  • 23,195
  • 2
  • 62
  • 85