Tuesday, March 4, 2014

Dependency injection in Sitecore event handlers

Following my previous article about Dependency injection in Sitecore custom commands, I think it is only appropriate to continue with something similar for Sitecore event handlers. And when I say similar I mean very similar - in fact reading this article after having read the previous one may induce some sense of deja vu :-) To learn more about Sitecore events visit Using Events.

This article assumes an understanding of Sitecore events and the concept of dependency injection. The purpose is to show how to use dependency injection in Sitecore events.

The normal way of creating an event handler for a Sitecore event is to create a handler class with an EventHandler delegate, i.e. a method with the EventHandler signature, and then add some config to Sitecore's <events> section defining where to find the event handler implementation, so that Sitecore can instantiate the event handler and trigger the delegate when the event occurs.

The problem with this approach is that nowadays it is common to use dependency injection in software solutions, and letting Sitecore take care of creating instances of your custom code means that you loose the possibility of injecting the needed dependencies. Luckily there is also a way out of this morass.

Sitecore has created a class called Event, which is used for subscribing, unsubscribing, and raising events. The good news is that it is available for use.

So here is a suggestion on how to use it to obtain dependency injection in Sitecore events. It is based on using Autofac as IoC container.

First, create a base class for your event handlers:

namespace TestApp.Events
{
  public abstract class BaseEventHandler
  {
    public string FullName { get; private set; }

    protected BaseEventHandler(string fullName)
    {
      FullName = fullName;
    }

    public abstract void OnEvent(object sender, System.EventArgs args);
  }
}
This base class defines the EventHandler delegate method signature that all derived event handler classes must implement, but it also has one property, FullName, for holding the event name for registration purposes.

Next, create your event handler inheriting from BaseEventHandler like this:
namespace TestApp.Events
{
  public class MyEventHandler : BaseEventHandler
  {
    private readonly IMyDependency _myDependency;

    public MyEventHandler(string fullName, IMyDependency myDependency)
      : base(fullName)
    {
      _myDependency = myDependency;
    }

    public override void OnEvent(object sender, System.EventArgs args)
    {
      // event handler implementation
    }
  }
}
As you can see we inject a dependency in the constructor. The constructor furthermore calls the base constructor to set the event name.

Now, create a class for registering events using the Sitecore Event class:
namespace TestApp.Events
{
  public static class EventConfigurator
  {
    public static void Configure(System.Collections.Generic.IEnumerable<BaseEventHandler> eventHandlers)
    {
      foreach (var eventHandler in eventHandlers)
      {
        Sitecore.Events.Event.Subscribe(eventHandler.FullName, eventHandler.OnEvent);
      }
    }
  }
}
The Configure method takes a collection of BaseEventHandler objects (our event handler instances), then uses Subscribe method on the Event class to subscribe the events.

That is basically all the pieces we need. We just have to fit everything together in our bootstrapper (the place where all the dependencies are set up using the IoC container). This could look something like this:
...
var builder = new ContainerBuilder();

builder.RegisterType<MyDependency>().As<IMyDependency>().InstancePerLifetimeScope();

builder.RegisterType<MyEventHandler>().As<BaseEventHandler>().WithParameter("fullName", "mynamespance:mycategory:myevent").InstancePerLifetimeScope();

var rootContainer = builder.Build();

var eventHandlers = rootContainer.Resolve<IEnumerable<BaseEventHandler>>();
EventConfigurator.Configure(eventHandlers);
...
So we just register dependencies as usual. The new thing is that we now register our event handlers in code, instead of using a Sitecore config file. And then we call our EventConfigurator with a collection of instances of all our event handlers.

That's it. Plain and simple :-)

Update:
Please note that since the event handlers are resolved only once (at app startup), any injected dependencies are effectively singletons.

5 comments:

Anonymous said...

Hi
This is a super useful post, I am trying to hook the saved event would you mind adding what goes into the "mynamespance:mycategory:myevent" with your example ?
Many thanks
Fred

Maze said...

It is actually just the event name, e.g. "item:saved" or name of custom event.

Chris said...

Sorry to necro this thread but I came across this today and whilst I wasn't content to be stuck with singleton lifetime it did inspire me to write this:

namespace Plumbing.IoC {
public class WindsorEventSubscriber
{
private readonly IKernel kernel;

public WindsorEventSubscriber(IKernel kernel)
{
this.kernel = kernel;
}

public void RegisterEvent(string assemblyName, string typeName, string methodName, string eventName)
{
var assembly = GetAssemblyByName(assemblyName);
var type = assembly.GetType(typeName);
var methodInfo = type.GetMethod(methodName);

EventHandler eventHandler = (object sender, EventArgs e) =>
{
var handlerObject = kernel.Resolve(type);
methodInfo.Invoke(handlerObject, new[] { sender, e });
};

Sitecore.Events.Event.Subscribe(eventName, eventHandler);
}

private static Assembly GetAssemblyByName(string name)
{
return AppDomain.CurrentDomain.GetAssemblies().
SingleOrDefault(assembly => assembly.GetName().Name == name);
}
}
}

By using closures it's possible to get transient (and possible PerWebRequest) lifetimes.

Maze said...

@Chris - I've had similar thoughts, only thing is that now you have an IoC container specific event subscriber, which kind of uses service location. But I agree it can definitely be an acceptable trade-off in order to get transient lifetimes if that is needed. Thank you for a good comment.

Chris said...

@Maze - It is indeed IoC specific but it'd be trivial to extract an interface with the RegisterEvent method as none of the parameters or return type is IoC specific. That way you could replace it with any IoC specific implementation you like. I also see this as being part of the plumbing code and as such is service location only to the same extent that your typical ControllerFactory implementation is in MVC.

Post a Comment