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

Raymund
  • 7,684
  • 5
  • 45
  • 78
  • 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. – Travis Illig Feb 27 '17 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 Feb 27 '17 at 20:44
  • BTW I updated the code above to show GetForeignKeysViewModelCache is under MyTestQuery Class – Raymund Feb 27 '17 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 Mar 01 '17 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 Mar 02 '17 at 01:20

1 Answers1

5

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.

Mickaël Derriey
  • 12,796
  • 1
  • 53
  • 57
  • 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 Feb 28 '17 at 00: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. – Mickaël Derriey Feb 28 '17 at 02:40
  • Same thing, there must be something in my setup if this is working on your end – Raymund Feb 28 '17 at 03: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 Mar 02 '17 at 01: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. – Mickaël Derriey Mar 02 '17 at 02:54
  • What I mean is that I am calling the method inside the same query hence its not triggering the interception properly. What that means is that I have call the primary query from the controller then the primary query calls the secondary query that has the cache attribute. But when I try to call the secondary query from the controller it works. – Raymund Mar 02 '17 at 03:20
  • Nearly there and apologies for not being clear enough. It also works for me that way but my scenario is GetSecondName is inside the class DefaultMyQuery where GetName is. So if I do this public string GetName() {return GetSecondName();} it does not work – Raymund Mar 02 '17 at 19:53
  • OK, got it. I'm afraid there's nothing you can do in that case, since when you're in `GetName` of your implementation class, you don't know anything about a potential interceptor. Can't you separate those 2 methods so that when you call `GetSecondName`, you do so through a separate instance of `IMySecondQuery`? – Mickaël Derriey Mar 03 '17 at 03:22
  • Thats what I figured out. Thanks for your help :) – Raymund Mar 05 '17 at 19:22