Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unity: Register two interfaces as one singleton with interception

I have a class that implements two interfaces, and I want to apply interception to the class's methods.

I'm following the advice in Unity Register two interfaces as one singleton, but I'm surprised by the results. In a nutshell, it seems that my CallHandler is called twice. The shortest example I have is this:

public interface I1
{
    void Method1();
}

public interface I2
{
    void Method2();
}

public class C : I1, I2
{
    [Log]
    public void Method1() {}

    public void Method2() {}
}

public class LogAttribute : HandlerAttribute
{
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return new LogCallHandler();
    }
}

public class LogCallHandler : ICallHandler
{
    public IMethodReturn Invoke(
        IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        Console.WriteLine("Entering " + input.MethodBase.Name);
        var methodReturn = getNext().Invoke(input, getNext);
        Console.WriteLine("Leaving " + input.MethodBase.Name);
        return methodReturn;
    }

    public int Order { get; set; }
} 

void Test()
{
    IUnityContainer container = new UnityContainer();
    container.AddNewExtension<Interception>();
    container.RegisterType<C>(new ContainerControlledLifetimeManager());

    container.RegisterType<I1, C>(
        new Interceptor<TransparentProxyInterceptor>(),
        new InterceptionBehavior<PolicyInjectionBehavior>());

    container.RegisterType<I2, C>(
        new Interceptor<TransparentProxyInterceptor>(),
        new InterceptionBehavior<PolicyInjectionBehavior>());

    container.Resolve<I1>().Method1();
}

Which gives this output:

Entering Method1
Entering Method1
Leaving Method1
Leaving Method1

Removing the "container.RegisterType I2, C" line makes the log appear only once. Adding a third interface, I3, which is similar to I2, causes the log to appear three times.

I would have expected the Log to be called only once. I can probably achieve this by having the LogCallHandler detect if it's being invoked from another LogCallHandler, but this seems inelegant.

Originally I wanted to apply the interception behavior to C rather than to I1 and I2 separately, but this requires C to inherit from MarshalByRefObject which is a constraint I'm not yet willing to impose.

Is there an alternative way?

like image 563
hibernator Avatar asked Mar 20 '11 09:03

hibernator


2 Answers

The problem is that you are applying the transparent proxy to each interface. Instead, if you apply it to the concrete class you get only one proxy. Also, you don't need to make it a singleton unless you want the instance to be shared.

I ran this configuration in a test console project and got the desired result. Kudos for including a working snippet that isolated your problem!

var container = new UnityContainer()
    .AddNewExtension<Interception>()
    .RegisterType<I1, C>()
    .RegisterType<I2, C>()
    .RegisterType<C>(
        new ContainerControlledLifetimeManager(),
        new Interceptor<TransparentProxyInterceptor>(),
        new InterceptionBehavior<PolicyInjectionBehavior>()
    );
like image 124
Mike Valenty Avatar answered Oct 27 '22 20:10

Mike Valenty


As it turns out, a small modification of my original snippet gives a solution:

public interface I1
{
    void Method1();
}

public interface I2
{
    void Method2();
}

public class C : I1, I2
{
    public int Data = 0;

    [Log]
    public void Method1() { Console.WriteLine("Method1 " + Data); Data = 1; }

    [Log]
    public void Method2() { Console.WriteLine("Method2 " + Data); Data = 2; }
}

public class LogAttribute : HandlerAttribute
{
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return new LogCallHandler();
    }
}

public class LogCallHandler : ICallHandler
{
    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        Console.WriteLine("Entering " + input.MethodBase.Name);
        var methodReturn = getNext().Invoke(input, getNext);
        Console.WriteLine("Leaving " + input.MethodBase.Name);
        return methodReturn;
    }

    public int Order { get; set; }
} 

void Test()
{
    IUnityContainer container = new UnityContainer();
    container.AddNewExtension<Interception>();
    container.RegisterType<C>(new ContainerControlledLifetimeManager());

    container.RegisterType<I1, C>();
    container.RegisterType<I2, C>();
    container.Configure<Interception>().SetInterceptorFor<I1>(new TransparentProxyInterceptor());
    container.Configure<Interception>().SetInterceptorFor<I2>(new TransparentProxyInterceptor());

    container.Resolve<I1>().Method1();
    container.Resolve<I2>().Method2();
    container.Resolve<C>().Method2();
}

The only difference is that I set the interception for I1 and I2 outside the call to RegisterType. The output of the above is:

Entering Method1
Method1 0
Leaving Method1
Entering Method2
Method2 1
Leaving Method2
Method2 2

Which gives me the following:

  • I intercept C's implementation of both I1 and I2.
  • There is only a single instance of C (as evidenced by the modification to the Data member).
  • I can reach the single instance of C (i.e. container.Resolve<C>() works, albeit without interception).

My use case here is unit-tests: my code relies on interception for some of its functionality (specifically, transaction management). C is a mock object that I provide to my tested class, and it implements two interfaces that the tested class needs. When the unit-test runs, I want to access the mock object to verify stuff on it.

The downside to the above solution is that I can only apply PolicyInjectionBehavior here (in fact, the code above doesn't specify any InterceptionBehavior: when using container.Configure<Interception>, the PolicyInjectionBehavior is automatically used by Unity, as far as I could tell. For my use case, it is acceptable.

I have to admit that I'm surprised that setting up interception during RegisterType gives different results than configuring it separately. If anyone can explain this, I'd be happy to understand why that is.

like image 34
hibernator Avatar answered Oct 27 '22 21:10

hibernator