Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Structuremap interception for registry scanned types

I have a ASP MVC 4 app that uses Structuremap. I'm trying to add logging to my application via Structuremap interception. In a Registry, I scan a specific assembly in order to register all of it's types with the default convention:

public class ServicesRegistry : Registry
{
    public ServicesRegistry()
    {
        Scan(x =>
        {
            x.AssemblyContainingType<MyMarkerService>();
            x.WithDefaultConventions();
        });
    }
}

The interceptor:

public class LogInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var watch = Stopwatch.StartNew();
        invocation.Proceed();
        watch.Stop();//log the time
    }
}

I can add the interceptor for one specific plugin type like this:

var proxyGenerator = new ProxyGenerator();
container.Configure(x => x.For<IServiceA>().Use<ServiceA>().DecorateWith(instance => proxyGenerator.CreateInterfaceProxyWithTarget(instance, new LogInterceptor())));

but I want to make structuremap create logging proxies for all the types that were scanned in the registry. Is there a way to achieve this?

like image 284
rinat Avatar asked Oct 02 '14 15:10

rinat


1 Answers

It doesn't look like there's an easy extension point for this, but I got it working with a fairly decent solution using a custom convention. In order to help you understand the decisions I made I'll walk you through a few steps (skipping the many, many missteps I made on my way).

First lets look at the DefaultConvention which you are already using.

DefaultConvention:

 public class DefaultConventionScanner : ConfigurableRegistrationConvention
{
    public override void Process(Type type, Registry registry)
    {
        if (!TypeExtensions.IsConcrete(type))
            return;
        Type pluginType = this.FindPluginType(type);
        if (pluginType == null || !TypeExtensions.HasConstructors(type))
            return;
        registry.AddType(pluginType, type);
        this.ConfigureFamily(registry.For(pluginType, (ILifecycle)null));
    }

    public virtual Type FindPluginType(Type concreteType)
    {
        string interfaceName = "I" + concreteType.Name;
        return Enumerable.FirstOrDefault<Type>((IEnumerable<Type>)concreteType.GetInterfaces(), (Func<Type, bool>)(t => t.Name == interfaceName));
    }
}

Pretty simple, we get the type and interface pairs and check to make sure they have a constructor, if they do we register them. It would be nice to just modify this so that it calls DecorateWith, but you can only call that on For<>().Use<>(), not For().Use().

Next lets look at what DecorateWith does:

public T DecorateWith(Expression<Func<TPluginType, TPluginType>> handler)
{
  this.AddInterceptor((IInterceptor) new FuncInterceptor<TPluginType>(handler, (string) null));
  return this.thisInstance;
}

So this creates a FuncInterceptor and registers it. I spent a fair bit of time trying to create one of these dynamically with reflection before deciding it would just be easier to make a new class:

public class ProxyFuncInterceptor<T> : FuncInterceptor<T> where T : class
{
    public ProxyFuncInterceptor() : base(x => MakeProxy(x), "")
    {
    }

    protected ProxyFuncInterceptor(Expression<Func<T, T>> expression, string description = null)
        : base(expression, description)
    {
    }

    protected ProxyFuncInterceptor(Expression<Func<IContext, T, T>> expression, string description = null)
        : base(expression, description)
    {
    }

    private static T MakeProxy(T instance)
    {
        var proxyGenerator = new ProxyGenerator();
        return proxyGenerator.CreateInterfaceProxyWithTarget(instance, new LogInterceptor());
    }
}

This class just makes it easier to work with when we have the type as a variable.

Finally I've made my own Convention based on the Default convention.

public class DefaultConventionWithProxyScanner : ConfigurableRegistrationConvention
{
    public override void Process(Type type, Registry registry)
    {
        if (!type.IsConcrete())
            return;
        var pluginType = this.FindPluginType(type);
        if (pluginType == null || !type.HasConstructors())
            return;
        registry.AddType(pluginType, type);
        var policy = CreatePolicy(pluginType);
        registry.Policies.Interceptors(policy);

        ConfigureFamily(registry.For(pluginType));
    }

    public virtual Type FindPluginType(Type concreteType)
    {
        var interfaceName = "I" + concreteType.Name;
        return concreteType.GetInterfaces().FirstOrDefault(t => t.Name == interfaceName);
    }

    public IInterceptorPolicy CreatePolicy(Type pluginType)
    {
        var genericPolicyType = typeof(InterceptorPolicy<>);
        var policyType = genericPolicyType.MakeGenericType(pluginType);
        return (IInterceptorPolicy)Activator.CreateInstance(policyType, new object[]{CreateInterceptor(pluginType), null});     
    }

    public IInterceptor CreateInterceptor(Type pluginType)
    {
        var genericInterceptorType = typeof(ProxyFuncInterceptor<>);
        var specificInterceptor = genericInterceptorType.MakeGenericType(pluginType);
        return (IInterceptor)Activator.CreateInstance(specificInterceptor);
    }
}

Its almost exactly the same with one addition, I create an interceptor and interceptorType for each type we register. I then register that policy.

Finally, a few unit tests to prove it works:

 [TestFixture]
public class Try4
{
    [Test]
    public void Can_create_interceptor()
    {
        var type = typeof (IServiceA);
        Assert.NotNull(new DefaultConventionWithProxyScanner().CreateInterceptor(type));
    }

    [Test]
    public void Can_create_policy()
    {
        var type = typeof (IServiceA);
        Assert.NotNull(new DefaultConventionWithProxyScanner().CreatePolicy(type));
    }

    [Test]
    public void Can_register_normally()
    {
        var container = new Container();
        container.Configure(x => x.Scan(y =>
        {
            y.TheCallingAssembly();
            y.WithDefaultConventions();
        }));

        var serviceA = container.GetInstance<IServiceA>();
        Assert.IsFalse(ProxyUtil.IsProxy(serviceA));
        Console.WriteLine(serviceA.GetType());
    }

    [Test]
    public void Can_register_proxy_for_all()
    {
        var container = new Container();
        container.Configure(x => x.Scan(y =>
        {
            y.TheCallingAssembly();
            y.Convention<DefaultConventionWithProxyScanner>();
        }));

        var serviceA = container.GetInstance<IServiceA>();
        Assert.IsTrue(ProxyUtil.IsProxy(serviceA));
        Console.WriteLine(serviceA.GetType());
    }

    [Test]
    public void Make_sure_I_wait()
    {
        var container = new Container();
        container.Configure(x => x.Scan(y =>
        {
            y.TheCallingAssembly();
            y.Convention<DefaultConventionWithProxyScanner>();
        }));

        var serviceA = container.GetInstance<IServiceA>();
        serviceA.Wait();
    }
}
}

 public interface IServiceA
{
    void Wait();
}

public class ServiceA : IServiceA
{
    public void Wait()
    {
       Thread.Sleep(1000);
    }
}

public interface IServiceB
{

}

public class ServiceB : IServiceB
{

}

There's definitely room for some clean up here (caching, make it DRY, more tests, make it easier to configure) but it works for what you need and is a pretty reasonable way of doing it.

Please ask if you have any other questions about it.

like image 98
JCalder Avatar answered Nov 09 '22 12:11

JCalder