Structuremap interception for registry scanned types
Asked Answered
M

1

15

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?

Mis answered 2/10, 2014 at 15:1 Comment(2)
Did you get this to work?Brahms
Unfortunately no. I added them manually for each of my plugin types.Mis
P
9

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.

Pugliese answered 26/9, 2015 at 3:46 Comment(3)
This looks good, I need time to refactor out the old strucutremap code as I had to revert an older release. This is a lot more code and it seems strange they have removed the ability to do this simply.Driggers
This just maid my day thanks for the grate answer and walk though the code. I'm using a newer version of structuremap (v4.0.30319) which required me to imploment the ScanTypes method in the DefaultConventionWithProxyScanner class but I just have that loop though the TypeSet and all the Process method which worked for me.Greylag
Oh I also go errors for generic classes such as IRepository<T> which cant have instances created for them but as I don't really want to log that code i'm ok but others may need to know of a way to get round this.Greylag

© 2022 - 2024 — McMap. All rights reserved.