2

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

5
  • 2
    You might need to show a query class example because it's unclear how the cache attribute is even used or why you'd think its use might be intercepted with what you have. Additional context would also be helpful - we can infer this is a web project of some nature - ASP.NET MVC? Web API? Don't assume we have the same context as you. Commented Feb 27, 2017 at 19:21
  • Hi Thanks for your reply. Cache Attribute is used to cache a Method, like on the example its the GetForeignKeysViewModelCache that is being cached. It just gets values on a database for all drop down basically its expensive hence I cache them. I am using MVC 5.2.3.0 not Web API. The caching works but the attribute value does not pass as normal, I can hard code the duration but I need a more flexible solution
    – Raymund
    Commented Feb 27, 2017 at 20:44
  • BTW I updated the code above to show GetForeignKeysViewModelCache is under MyTestQuery Class
    – Raymund
    Commented Feb 27, 2017 at 20:49
  • 1
    I've copy pasted your code with libraries of versions you provided and all works fine for me (cacheAttribute is not null). Maybe you need provide some more details.
    – Evk
    Commented Mar 1, 2017 at 21:15
  • It works now, its just stupidity on my end. I am using interceptor and expecting it to intercept something from Query to Query.
    – Raymund
    Commented Mar 2, 2017 at 1:20

1 Answer 1

5
+100

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.

9
  • I moved it to the interface level still no attribute value. Same results the CacheResultInterceptor triggers but no value passed. Just to clarify this invocation.MethodInvocationTarget and this invocation.Method returns something only the attribute is null.
    – Raymund
    Commented Feb 28, 2017 at 0:49
  • they both return someting, true, but from your code, MethodInvocationTarget takes precendence over Method as Method will be used only if MethodInvocationTarget returns null. Try using Method only. Commented Feb 28, 2017 at 2:40
  • Same thing, there must be something in my setup if this is working on your end
    – Raymund
    Commented Feb 28, 2017 at 3:30
  • Thanks for the detailed explanation, I exactly did it the same way and it works. The only problem on my side is I am not calling the cached query in the controller but I am calling it on the query itself. Ie in your example MyQuery is not called from HomeController but from MySecondQuery. In this case how do I do it?
    – Raymund
    Commented Mar 2, 2017 at 1:12
  • Could you update your question with the specific scenario? I'm having trouble understanding this. Since you intercept all the type that end with Query, the first query is cached but it also calls the second query which is also cached? Anyway, in the end it shouldn't make a difference, as long as the query is intercepted, it doesn't matter where it's being called from. Hope that makes sense. Commented Mar 2, 2017 at 2:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.