Intro

Once I have worked with Orchard CMS. Orchard has well implemented features. One of most liked by me is event system.

How it works?

Consider I have interface that represents some events:

interface IUserEvent : IEvent
{
  void Created(string id);
  void Updated(string id);
}

So let’s create some handlers:

class LogUserEvents : IUserEvent
{
  public void Create(string id)
  {
    someLogger.Log($"User { id } created");
  }

  public void Updated(string id)
  {
    someLogger.Log($"User { id } updated");
  }
}

And usage

class SomeService
{
    private readonly IUserEvents userEvents;
    
    public SomeService(IUserEvents userEvents)
    {
      this.userEvents = userEvents;
    }
}

And… that’s all! The Event bus system automatically searches for implementations, resolves it with the current dependency resolver and calls proper methods. Magic!

How Can I achieve it?

The first step is to create a proxy class for interfaces, that automatically invokes all implementations. I have used Castle.DynamicProxy for it:

public class MultiInterfaceProxy : IInterceptor
{
    private readonly Lazy<IEnumerable<IEvent>> events;

    private MultiInterfaceProxy(Type eventType, 
                                IImplementationProvider implementationProvider)
    {
        this.events = implementationProvider.For(eventType);
    }

    private IEnumerable<IEvent> Events
    {
        get { return this.events.Value; }
    }

    public static object For(Type eventType, 
                            IImplementationProvider implementationProvider)
    {
        if (!eventType.IsInterface)
            throw new ArgumentException("Event must be an interface.", "eventType");

        if (!typeof(IEvent).IsAssignableFrom(eventType))
            throw new ArgumentException("Event must be derrived from IEvent", "eventType");

        var generator = new ProxyGenerator();
        var proxtInterceptor =
            new MultiInterfaceProxy(eventType,
                                    implementationProvider);

        return generator.CreateInterfaceProxyWithoutTarget(eventType, proxtInterceptor);
    }


    public void Intercept(IInvocation invocation)
    {
        var returnType = invocation.Method.ReturnType;

        if (returnType.IsGenericType &&
            typeof(IEnumerable<>).IsAssignableFrom(returnType.GetGenericTypeDefinition()))
        {
            this.InvokeWithEnumerableReturn(invocation, returnType);
        }
        else if (returnType == typeof (Task))
        {
            this.HandleTask(invocation);
        }
        else
        {
            this.InvokeVoid(invocation);
        }
    }

    private void HandleTask(IInvocation invocation)
    {
        var tasks = this.Events.Select(ev => (Task) invocation.Method.Invoke(ev,
            invocation.Arguments)).ToArray();


        invocation.ReturnValue = Task.Factory.StartNew(() => Task.WaitAll(tasks));
    }

    private void InvokeVoid(IInvocation invocation)
    {
        object result = null;

        foreach (var @event in this.Events)
        {
            try
            {
                result = invocation.Method.Invoke(@event, invocation.Arguments);
            }
            catch (Exception ex)
            {
                HandleException(ex, @event);
            }
        }

        invocation.ReturnValue = result;
    }

    private void InvokeWithEnumerableReturn(IInvocation invocation, 
                                            Type returnType)
    {
        var list =
            (IList)
            Activator.CreateInstance(typeof(List<>)
            .MakeGenericType(returnType.GetGenericArguments()[0]));

        foreach (var @event in this.Events)
        {
            try
            {
                var enumerable = (IEnumerable)invocation.Method.Invoke(@event, invocation.Arguments);
                if (enumerable == null)
                {
                    continue;
                }

                var enumerator = enumerable.GetEnumerator();

                while (enumerator.MoveNext())
                {
                    list.Add(enumerator.Current);
                }
            }
            catch (Exception ex)
            {
                HandleException(ex, @event);
            }
        }

        invocation.ReturnValue = list;
    }

    private static void HandleException(Exception ex, IEvent proxiedEvent)
    {
        if (!(ex is TargetInvocationException))
        {
            throw ex;
        }

        var tex = ex.InnerException;
        if (tex == null)
        {
            throw ex;
        }

        if (!(tex is EventFatalException) && !proxiedEvent.IsAlwaysFatal())
        {
            return;
        }

        if (tex.InnerException != null)
        {
            tex = tex.InnerException;
        }

        throw tex;
    }
}

(part of classes are skipped).

But alone proxy is not enough - we need to integrate it with some dependency injection library. I have chosen my favourite - AutoFac:

foreach (var eventType in this.GetAllEventTypes())
{
    var localEventType = eventType;
    var eventTypeKey = new object();

    builder.RegisterAssemblyTypes(this.assemblies)
           .AssignableTo(localEventType)
           .As(localEventType)
           .InstancePerLifetimeScope()
           .Keyed(eventTypeKey, eventType);

    builder.Register(ctx => MultiInterfaceProxy.For(localEventType,
                                                    new ComponentContextProvider(ctx, eventTypeKey)))
           .As(localEventType).InstancePerLifetimeScope();
}

What do you think about it?

Full code available on github.


Patryk Wąsiewicz

A very casual blog about programmer's life.