7

I have an IoC setup with Autofac and use AoP interceptors.

Normally, I use interface interceptors registered like this:

var builder = new ContainerBuilder();
builder.RegisterType<MyType>()
    .As<IMyType>()
    .UsingConstructor(new Type[0])
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(MyInterceptor));

And it works. But for certain reasons (not apparent in this minimal example), I need one class to be registered and injected also as self (not through interface), so I try:

var builder = new ContainerBuilder();
builder.RegisterType<MyType>()
    .As<IMyType>()
    .AsSelf()
    .UsingConstructor(new Type[0])
    .EnableClassInterceptors()
    .InterceptedBy(typeof(MyInterceptor));

In this setup, the interceptor is never fired. When I inspect the injected dependency in debug, it does seem to be a subclassed proxy (as it should), but its _interceptors private property only contains a single instance of Castle.DynamicProxy.StandardInterceptor, which is obviously not what I configured.

In fact, if I remove AsSelf() it still doesn't intercept, which leads me to a conclusion that either I'm doing something wrong, or class interception simply doesn't work... ?

UPDATE

MyType is actually inheriting from EF DbContext and I'm trying to intercept SaveChanges(), which is virtual. Just for test I added public virtual void Foo() which also wasn't intercepted.

UPDATE

I overlooked this earlier and to simplify skipped an important fact: I am specifying UsingConstructor() in the registration. Now I empirically found out that UsingConstructor() seems to prevent EnableClassInterceptors() from working.

Full registration I'm currently using:

    builder.RegisterType<FooClass>()
        .AsSelf()
        .InstancePerRequest()
        .EnableClassInterceptors()
        .UsingConstructor(new Type[0]) // commenting this out solves the issue
        .InterceptedBy(typeof(MyInterceptor));

public class FooClass
{
    public virtual void Bar()
    {
        Debugger.Break();
    }
    public FooClass() { }
    public FooClass(int i) { }
}

Interceptor works for other injections; it's complex code but I put breakpoint at start of public void Intercept(IInvocation invocation) method.

Commenting out the constructor choice makes it work again.

I will award the bounty to anyone who can give me a workaround, or at least a good explanation of why this doesn't work.

UPDATE

With your answers about added constructor parameters, I investigated this aspect and indeed:

Foo.GetType().GetConstructors() // Foo is injected, proxied instance of FooClass

returns in fact 3 constructors:

Void .ctor(Castle.DynamicProxy.IInterceptor[])
Void .ctor(Castle.DynamicProxy.IInterceptor[], Int32)
Void .ctor()

This happens whether I add UseConstructor() or not. By coincidence, the code didn't throw for me; it would if I specified the other constructor (not the parameterless).

Regardless, I tried the following registration:

    builder.RegisterType<MyType>()
        .As<IMyType>()
        .AsSelf()
        .InstancePerRequest()
        .UsingConstructor(new[] { typeof(IInterceptor[]) })
        .EnableClassInterceptors()
        .InterceptedBy(typeof(MyInterceptor));

... and, surprise, surprise! it fails with

No matching constructor exists on type MyType

Digging further, changing the order of those fluent methods solves it ultimately, with the complete working registration being:

    builder.RegisterType<MyType>()
        .As<IMyType>()
        .AsSelf()
        .InstancePerRequest()
        .EnableClassInterceptors()
        .UsingConstructor(new[] { typeof(IInterceptor[]) })
        .InterceptedBy(typeof(MyInterceptor));

SUMMARY

I would consider it bad API to have what seems to be fluent API but yet depend significantly on the order of invoking fluent methods. It's even worse that it doesn't actually throw, just fails silently.

I would also consider it bad design (leave alone documentation) to require from users internal knowledge of proxying logic. Ideally, UsingConstructor() should encapsulate in the matching logic the added fact of interceptors. That said, I designed APIs myself and I know it's easy to demand but difficult to deliver certain features.

Anyway, the case is closed and I would like to give hug to all of you. I believe it was Jim Bolla that gave first precise answer that led to the breakthrough, so cookies go to him. Correct me if I'm wrong.

Jacek Gorgoń
  • 3,206
  • 1
  • 26
  • 43
  • I have a similar issue at the moment, curious for the feedback on this one... – Segers-Ian Aug 22 '14 at 15:15
  • Can you post the contents of `MyType`? Per the docs (http://autofac.readthedocs.org/en/latest/advanced/interceptors.html) you can only use class interception on virtual methods. I'm curious to see if the methods in your target type are virtual. – Travis Illig Aug 23 '14 at 17:45
  • Updated question; yes, it is virtual. – Jacek Gorgoń Aug 24 '14 at 18:58
  • 2
    @JacekGorgoń - class interception *does* work, there are unit tests in the Autofac project to prove that. I set up tests to try and simulate your setup (AsSelf, As interface, and subclassing another class with virtual methods) and it all works as expected here. Could you update your question with 1) the source of `MyInterceptor` and 2) the versions of Autofac, Autofac.Extras.DynamicProxy2 and Entity Framework ? – Peter Lillevold Aug 25 '14 at 10:47
  • One more thing you can try: Use DynamicProxy directly to subclass `MyType`, eliminating Autofac from the equation. This way you can verify that `MyType` is proxyable. The answer to that at least narrows down the source of your problem. – Jim Bolla Aug 25 '14 at 15:44
  • Thanks for hints, I should have more time today to debug this again. MyType is apparently proxyable, because in debug the injected instance is in fact a proxy. – Jacek Gorgoń Aug 26 '14 at 07:08
  • 2
    I seem to have narrowed down on the culprit, but still lack explanation or workaround. See update. – Jacek Gorgoń Aug 26 '14 at 14:37
  • I agree, @JimBolla answer was the breakthrough we needed :) – samy Aug 27 '14 at 10:48
  • Another comment; you should consider pushing an issue to the autofac github: repro is easy so a failing test would be possible (or you could point to this thread). Perhaps even two issues: one for the interception problem, and one for the fluent declaration depending on order. Even if they won't fix, it would document the problem. – samy Aug 27 '14 at 10:52
  • The issue lies with how DynamicProxy class proxies work, Because it's subclassing your type and changing the constructors, and relying on those constructors in order to inject the interceptors. I think Autofac should just state "don't use UsingConstructor()" with class proxies. It might be able to check that at runtime, but this seems like one of those rare edge cases. Better documentation is definitely in order, at least. – Jim Bolla Aug 27 '14 at 12:03
  • Another issue is that the ordering on those fluent config methods is significant - it shouldn't be. Instead, all logic should be deferred to actual construction point when all configuration is applied. – Jacek Gorgoń Aug 27 '14 at 12:08
  • 1
    Autofac documentation has been updated with verbiage to avoid UsingConstructor() with EnableClassInterceptors(): http://autofac.readthedocs.org/en/latest/advanced/interceptors.html#tips – Jim Bolla Aug 27 '14 at 19:16
  • @JimBolla What was added to the docs explain the problem, but there could be an indication on how to circumvent it if really needed (ie, change the constructor signature with the added parameter) – samy Sep 12 '14 at 13:17

2 Answers2

6

When you use EnableClassInterceptors(), Autofac tells Castle Dynamic Proxy to subclass your type. This new subtype gets new constructor signatures that add a parameter of type IInterceptor[]. Meanwhile, Autofac by default, uses its MostParametersConstructorSelector to pick the constructor to use when activating a type. So normally it will match on the constructor that has the IInterceptor[] parameter.

When you call UsingConstructor(), the registration changes to use a MatchingSignatureConstructorSelector matching the parameter types you specified. (In your case, none.) This causes Autofac to not use the constructor that accepts the IInterceptor[] parameter, thus causing your interceptors to not be passed into the proxy.

I'm actually surprised it doesn't throw an exception for not having a matching constructor, because that's what the code looks like should happen. That part I still haven't solved.

Jim Bolla
  • 8,265
  • 36
  • 54
  • +1 for the insight, I found the correct way to specify constructors parameters thanks to this – samy Aug 27 '14 at 07:11
  • When proxying a class, the proxy keeps a parameterless constructor (which is mandatory to create a proxy iirc). That's why Autofac doesn't fail with the constructor – samy Aug 27 '14 at 08:05
2

EDIT: the answer by Jim Bolla is right - using EnableClassInterceptors creates a subtype of MyType that will change existing constructors to add a IInterceptor[] parameter (along with a parameterless constructor). Then the interceptors you declare in InterceptedBy are passed to the subtype.

When you add the UsingConstructor method, Autofac will create the subtype using the parameterless constructor which will not register the interceptors declared. That is why you don't have the expected behavior. It is possible to make it work by forcing Autofac to resolve the constructor with the new signature. Like this:

var otherBuilder = new ContainerBuilder();
otherBuilder.RegisterType<MyType>()
    .As<IMyType>()
    .AsSelf()
    .EnableClassInterceptors()
    .InterceptedBy(typeof(MyInterceptor))
    .UsingConstructor(new Type[1] { typeof(IInterceptor[]) })
    ;

This will work as intended. This behavior frankly surprises me, I would have thought that interceptors would be added after construction in a second step by Autofac/DynamicProxy. I'm wondering if this could be seen as a bug or at least a surprising behavior. The solution above smells a bit since it is not obvious at all that you have to change the constructor signature in order to accomodate the proxy.

If you need to use a constructor that has parameters, the IInterceptor[] parameter is always added first in the inherited constructors so for example using the int-based constructor you would write: .UsingConstructor(new Type[2] {typeof(IInterceptor[]), typeof(int) })


I think there may be a problem regarding the method you declare as virtual and/or you want to intercept: here some test code that shows that the interceptor is called against an interface and a class:

public interface IMyType { void method(); }
public class MyType: IMyType {
    public virtual void method() { Console.WriteLine("method"); }
    public virtual void method2() { Console.WriteLine("method2"); }
}

public class MyInterceptor : MethodInterceptor
{
    protected override void PreProceed(IInvocation invocation)
    {
        Console.Write("before - ");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<MyType>()
            .As<IMyType>()
            .EnableInterfaceInterceptors()
            .InterceptedBy(typeof(MyInterceptor));
        builder.RegisterType<MyInterceptor>()
            .AsSelf();
        var container = builder.Build();

        var otherBuilder = new ContainerBuilder();
        otherBuilder.RegisterType<MyType>()
            .AsSelf()
            .As<IMyType>()
            .EnableClassInterceptors()
            .InterceptedBy(typeof(MyInterceptor));
        otherBuilder.RegisterType<MyInterceptor>()
            .AsSelf();
        var otherContainer = otherBuilder.Build();

        container.Resolve<IMyType>().method();
        // outputs -> before - method
        otherContainer.Resolve<IMyType>().method();
        // outputs -> before - method
        otherContainer.Resolve<MyType>().method();
        // outputs -> before - method
        otherContainer.Resolve<MyType>().method2();
        // outputs -> before - method2
    }
}

As you can see, the second registration calls the interceptor in all cases:

  • resolving by interface and calling the interface method
  • resolving by class and calling the interface method
  • resolving by class and calling the class method

Are you sure the registration is correct? What do you resolve against in the second case (IMyType or MyType)?

samy
  • 14,832
  • 2
  • 54
  • 82