Search code examples
c#autofaccode-injectioncastleinterception

Getting Attribute Value on Member Interception


I have this CacheAttribute that accepts Duration Value like such

public class MyTestQuery : IMyTestQuery
{
    private readonly ISomeRepository _someRepository;

    public TestQuery(ISomeRepository someRepository)
    {
        _someRepository = someRepository;
    }

    [Cache(Duration = 10)]
    public MyViewModel GetForeignKeysViewModelCache()
    {
        ...code here...
        return viewModel;
    }
}

The Attribute looks like this

[AttributeUsage(AttributeTargets.Method)]
public class CacheAttribute : Attribute
{
    public int Duration { get; set; }
}

When Intercepted using Castle.Proxy.IInterceptor it works but when I perform an Attribute.GetCustomAttribute either by IInvocation.MethodInvocationTarget or IInvocation.Method both returns a null value

Here it is in code

public class CacheResultInterceptor : IInterceptor
{
    public CacheAttribute GetCacheResultAttribute(IInvocation invocation)
    {
        var methodInfo = invocation.MethodInvocationTarget;
        if (methodInfo == null)
        {
            methodInfo = invocation.Method;
        }

        return Attribute.GetCustomAttribute(
            methodInfo,
            typeof(CacheAttribute),
            true
        )
        as CacheAttribute;
    }
    public void Intercept(IInvocation invocation)
    {
        var cacheAttribute = GetCacheResultAttribute(invocation);
        //cacheAttribute is null always

        ...more code here...
    }
}

And this is how I register them

public class Bootstrapper
{
    public static ContainerBuilder Builder;

    public static void Initialise()
    {
        Builder = new ContainerBuilder();

        ...other codes in here...
        CacheInstaller.Install();

        var container = Builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }

}

public class CacheInstaller 
{
    public static void Install()
    {

        Bootstrapper.Builder.RegisterType<CacheResultInterceptor>()
            .SingleInstance();

        Bootstrapper.Builder.RegisterAssemblyTypes(Assembly.Load("MyApplication.Web"))
            .Where(t => t.Name.EndsWith("Query"))
            .AsImplementedInterfaces()
            .EnableInterfaceInterceptors()
            .InterceptedBy(typeof(CacheResultInterceptor))
            .SingleInstance();

    }
}

My Expensive Method Class Ends with Query

Now the question is why invocation.MethodInvocationTarget and/or invocation.Method returns null? What am I doing wrong? Any other strategies so I can pass a parameter value without creating a Method for each value I can think of?

BTW I am using

  • Autofac 4.3.0.0
  • Autofac.Extras.DynamicProxy 4.2.1.0
  • Autofac.Integration.Mvc 4.0.0.0
  • Castle.Core 4.0.0.0

UPDATE 1 Here is what it returns when it runs for clarity

enter image description here


Solution

  • Here's what I found.

    invocation.Method returns the method declaration on the interface, in your case IMyTestQuery.

    On the other hand, invocation.MethodInvocationProxy returns the method that is going to be called when invoking invocation.Proceed(). This means it can be:

    • the next interceptor if you have several
    • a decorator if you have decorators over your interface
    • the final implementation of your interface

    As you can see, MethodInvocationProxy is less deterministic than Method, which is why I would recommend you avoid using it, at least for what you're trying to achieve.

    When you think about it, an interceptor should not be tied to an implementation as it proxies an interface, so why don't you put the [Cache] attribute at the interface level?

    Using your code, I could successfully retrieve it when put on the interface.


    Edit:

    OK, I've put together a repository on GitHub that uses the specific versions of the NuGet packages you mentioned and shows how to retrieve an attribute on intercepted methods.

    As a reminder, here are the used NuGet packages:

    • Microsoft.AspNet.Mvc v5.2.3
    • Autofac v4.3.0
    • Autofac.Mvc5 4.0.0
    • Autofac.Extras.DynamicProxy v4.2.1
    • Castle.Core v4.0.0

    I created 2 query interfaces, IMyQuery and IMySecondQuery. Please note that as mentioned in my original answer, the [Cache] attributes are placed on the interfaces methods, not on the implementing classes.

    public interface IMyQuery
    {
        [Cache(60000)]
        string GetName();
    }
    
    public interface IMySecondQuery
    {
        [Cache(1000)]
        string GetSecondName();
    }
    

    Then we have 2 very basic implementations of these classes. Not relevant at all, but for the sake of completeness:

    public class DefaultMyQuery : IMyQuery
    {
        public string GetName()
        {
            return "Raymund";
        }
    }
    
    public class DefaultMySecondQuery : IMySecondQuery
    {
        public string GetSecondName()
        {
            return "Mickaël Derriey";
        }
    }
    

    And then the interceptor:

    public class CacheResultInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            var cacheAttribute = invocation.Method.GetCustomAttribute<CacheAttribute>();
            if (cacheAttribute != null)
            {
                Trace.WriteLine($"Found a [Cache] attribute on the {invocation.Method.Name} method with a duration of {cacheAttribute.Duration}.");
            }
    
            invocation.Proceed();
        }
    }
    

    Note that the GetCustomAttribute<T> method is an extension method over MemberInfo present in the System.Reflection namespace.

    Let's move on to the registration in the Autofac container. I tried to follow you registration style as much as I could:

    var builder = new ContainerBuilder();
    
    builder.RegisterControllers(typeof(MvcApplication).Assembly);
    
    builder
        .RegisterType<CacheResultInterceptor>()
        .SingleInstance();
    
    builder
        .RegisterAssemblyTypes(typeof(MvcApplication).Assembly)
        .Where(x => x.Name.EndsWith("Query"))
        .AsImplementedInterfaces()
        .EnableInterfaceInterceptors()
        .InterceptedBy(typeof(CacheResultInterceptor));
    
    DependencyResolver.SetResolver(new AutofacDependencyResolver(builder.Build()));
    

    The queries are then used in the HomeController:

    public class HomeController : Controller
    {
        private readonly IMyQuery _myQuery;
        private readonly IMySecondQuery _mySecondQuery;
    
        public HomeController(IMyQuery myQuery, IMySecondQuery mySecondQuery)
        {
            _myQuery = myQuery;
            _mySecondQuery = mySecondQuery;
        }
    
        public ActionResult MyQuery()
        {
            return Json(_myQuery.GetName(), JsonRequestBehavior.AllowGet);
        }
    
        public ActionResult MySecondQuery()
        {
            return Json(_mySecondQuery.GetSecondName(), JsonRequestBehavior.AllowGet);
        }
    }
    

    What I did to test this is just put a breakpoint in the interceptor, F5 the application, open a browser and navigate to both http://localhost:62440/home/myquery and http://localhost:62440/home/myquery.

    It did hit the interceptor and find the [Cache] attribute. In the Visual Studio Output window, it did show:

    Found a [Cache] attribute on the GetName method with a duration of 60000.
    Found a [Cache] attribute on the GetSecondName method with a duration of 1000.
    

    Hopefully that helps you pinpoint what's going on in your project.


    I pushed changes to the repository so that the first query calls the second one.

    It still works. You should really make an effort and put some code on the question.