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.
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.
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:
Are you sure the registration is correct? What do you resolve against in the second case (IMyType
or MyType
)?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With