Search code examples
c#.netasp.net-mvcaopstructuremap3

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?


Solution

  • 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.