Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac global interface interceptor with Autofac.Extras.DynamicProxy2

Tags:

c#

autofac

I'm using interface interceptors with Autofac DynamicProxy2 and I'm able to enable interface interceptors per register:

   var registration = builder.RegisterType<AType>().As<AInterface>();
   registration.EnableInterfaceInterceptors().InterceptedBy<AInterceptor>()

I would like to apply certain trait to all registered types. Something like:

   var registrations = builder.GetAllRegistrations(); // ops, this does not exist...
   foreach (var registration in registrations) {
       registration.EnableInterfaceInterceptors().InterceptedBy<AInterceptor>()
   }

I cannot find a way to get all registrations. I'm aware we can do:

   builder.RegisterCallback(cr =>
   {
       foreach (var registration in cr.Registrations)
       {
            // registration is IComponentRegistration
       }
   });

But the registration here is a IComponentRegistration and I need a IRegistrationBuilder to apply the EnableInterfaceInterceptors().

like image 284
tozevv Avatar asked Apr 01 '14 09:04

tozevv


2 Answers

You can add the interceptors dynamically but it requires a little work. The way to go is to create a custom Autofac.Module that attaches to all component registrations. I'll show it to you in an example.

You can't really do EnableInterfaceInterceptors globally. I'll get to that at the end of the example.

First, the example setup: We have a simple interface, a simple implementation, and an interceptor that will handle logging calls. (I'm stealing the interceptor code from the Autofac wiki):

public interface IInterface
{
  void DoWork();
}

public class Implementation : IInterface
{
  public void DoWork()
  {
    Console.WriteLine("Implementation doing work.");
  }
}

public class CallLogger : IInterceptor
{
  TextWriter _output;

  public CallLogger(TextWriter output)
  {
    _output = output;
  }

  public void Intercept(IInvocation invocation)
  {
    _output.WriteLine("Calling method {0} with parameters {1}... ",
      invocation.Method.Name,
      string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));

    invocation.Proceed();

    _output.WriteLine("Done: result was {0}.", invocation.ReturnValue);
  }
}

We want to intercept everything (that has interceptors enabled) with our call logger. We do that by creating a custom Autofac.Module that both registers the interceptor itself with Autofac and attaches to component registrations on the fly to add the interceptor metadata.

Warning: There's a little hacking here that works but is sort of "poking data" into a sort-of-known-location. It works and I don't know why it'd change, but be aware that because it's sorta-kinda working on "private-ish" stuff, this may break in a future release. Just be aware.

OK, disclaimer done. Here's the module:

public class InterceptorModule : Autofac.Module
{
  // This is a private constant from the Autofac.Extras.DynamicProxy2 assembly
  // that is needed to "poke" interceptors into registrations.
  const string InterceptorsPropertyName = "Autofac.Extras.DynamicProxy2.RegistrationExtensions.InterceptorsPropertyName";

  protected override void Load(ContainerBuilder builder)
  {
    // Register global interceptors here.
    builder.Register(c => new CallLogger(Console.Out));
  }

  protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
  {
    // Here is where you define your "global interceptor list"
    var interceptorServices = new Service[] { new TypedService(typeof(CallLogger)) };

    // Append the global interceptors to any existing list, or create a new interceptor
    // list if none are specified. Note this info will only be used by registrations
    // that are set to have interceptors enabled. It'll be ignored by others.
    object existing;
    if (registration.Metadata.TryGetValue(InterceptorsPropertyName, out existing))
    {
      registration.Metadata[InterceptorsPropertyName] =
        ((IEnumerable<Service>)existing).Concat(interceptorServices).Distinct();
    }
    else
    {
      registration.Metadata.Add(InterceptorsPropertyName, interceptorServices);
    }
  }
}

To make it work, you register the module along with the rest of your dependencies. For this example, that'd look like: var builder = new ContainerBuilder();

// Notice this registration doesn't include
// the interceptor - that gets added by the
// module.
builder.RegisterType<Implementation>()
       .As<IInterface>()
       .EnableInterfaceInterceptors();

// Here's the magic module:
builder.RegisterModule<InterceptorModule>();
var container = builder.Build();

If you run these registrations and resolve...

var impl = container.Resolve<IInterface>();
impl.DoWork();

You can see the interceptor works as you'll see the console output:

Calling method DoWork with parameters ... 
Implementation doing work.
Done: result was .

(It's a little odd because I have a parameterless/void method in my example, but the interceptor is working!)

As for the EnableInterfaceInterceptors call... Doing EnableInterfaceInterceptors or EnableClassInterceptors actually does a lot of crazy DynamicProxy2 work on the back end. It adds some non-trivial event handlers to the activation event on your component that wrap the object in a dynamic proxy. These event handlers are not currently exposed for separate usage and I'm not sure how much work it'd be to try and attach all these things "after the fact" the way we're doing here with the actual interceptors.

You are welcome to give it a try yourself - the source is on GitHub. But, basically, while the "add a global interceptor" thing works, doing global EnableInterfaceInterceptors in a module is well off the beaten trail. You'd definitely be on your own for that one.

like image 149
Travis Illig Avatar answered Sep 18 '22 06:09

Travis Illig


Apparently is not trivial at all to drop the EnableInterfaceInterceptors call at the moment we perform the registry.

Since my goal was to implement a interceptor common to all interfaces resolved through Autofac I ended up rolling my own solution inspired on the DynamicProxy implementation from Autofac.

The idea is to override the Autofac Activating event and create the DynamicProxy manually. Most of the work here is making sure we can safely proxy the type being resolved.

public static class AutofacExtensions
{
    // DynamicProxy2 generator for creating proxies
    private static readonly ProxyGenerator generator = new ProxyGenerator();

    /// <summary>
    /// Intercept ALL registered interfaces with provided interceptors.
    /// Override Autofac activation with a Interface Proxy.
    /// Does not intercept classes, only interface bindings.
    /// </summary>
    /// <param name="builder">Contained builder to apply interceptions to.</param>
    /// <param name="interceptors">List of interceptors to apply.</param>
    public static void InterceptInterfacesBy(this ContainerBuilder builder, params IInterceptor[] interceptors)
    {
        builder.RegisterCallback((componentRegistry) =>
        {
            foreach (var registration in componentRegistry.Registrations)
            {
                InterceptRegistration(registration, interceptors);
            }
        });
    }

    /// <summary>
    /// Intercept a specific component registrations.
    /// </summary>
    /// <param name="registration">Component registration</param>
    /// <param name="interceptors">List of interceptors to apply.</param>
    private static void InterceptRegistration(IComponentRegistration registration, params IInterceptor[] interceptors)
    {
        // proxy does not get allong well with Activated event and registrations with Activated events cannot be proxied.
        // They are casted to LimitedType in the IRegistrationBuilder OnActivated method. This is the offending Autofac code:
        // 
        // public IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> OnActivated(Action<IActivatedEventArgs<TLimit>> handler)
        // {
        //    if (handler == null) throw new ArgumentNullException("handler");
        //    RegistrationData.ActivatedHandlers.Add(
        //        (s, e) => handler(new ActivatedEventArgs<TLimit>(e.Context, e.Component, e.Parameters, (TLimit)e.Instance)));
        //    return this;
        // }
        Delegate[] handlers = GetActivatedEventHandlers(registration);
        if (handlers.Any(h => handlers[0].Method.DeclaringType.Namespace.StartsWith("Autofac")))
        {
            return;
        }

        registration.Activating += (sender, e) =>
        {
            Type type = e.Instance.GetType();

            if (e.Component.Services.OfType<IServiceWithType>().Any(swt => !swt.ServiceType.IsInterface || !swt.ServiceType.IsVisible) || 
                // prevent proxying the proxy 
                type.Namespace == "Castle.Proxies")
            {
                return;
            }

            var proxiedInterfaces = type.GetInterfaces().Where(i => i.IsVisible).ToArray();

            if (!proxiedInterfaces.Any())
            {
                return;
            }

            // intercept with all interceptors
            var theInterface = proxiedInterfaces.First();
            var interfaces = proxiedInterfaces.Skip(1).ToArray();

            e.Instance = generator.CreateInterfaceProxyWithTarget(theInterface, interfaces, e.Instance, interceptors);
        };
    }

    /// <summary>
    /// Get Activated event handlers for a registrations
    /// </summary>
    /// <param name="registration">Registration to retrieve events from</param>
    /// <returns>Array of delegates in the event handler</returns>
    private static Delegate[] GetActivatedEventHandlers(IComponentRegistration registration)
    {
        FieldInfo eventHandlerField = registration.GetType().GetField("Activated", BindingFlags.NonPublic | BindingFlags.Instance);
        var registrations = eventHandlerField.GetValue(registration);
        System.Diagnostics.Debug.WriteLine(registrations);
        return registrations.GetType().GetMethod("GetInvocationList").Invoke(registrations, null) as Delegate[];
    }
}
like image 27
tozevv Avatar answered Sep 20 '22 06:09

tozevv