Better event handling in Episerver

In January I wrote an article which documented Episerver content events. As it was seen from this article, there are plenty of events available and those have different event arguments with different properties where not all of those properties are used. This makes the API hard to use.

When thinking about Episerver content events as an outer border of your application according to the ports and adapters architecture (even known as a pizza architecture :)), then we have to implement some adapter for these events. This adapter should translate Episerver events into our application's events.

There is a good solution for such purpose. Some time ago Valdis Iļjučonoks wrote an article how Mediator pattern can help with this. I am going to use MediatR library for this purpose.

First of all, install MediatR in your project.

Install-Package MediatR

Then there will be a need for an initialization module where the events will be handled. Create one, in the Initialize method load IContentEvents and attach an event handler to the events you care. In this example, I am attaching to the SavedContent event. Do not forget to detach the events in the Uninitialize method.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class EventInitialization : IInitializableModule
{
    private static bool _initialized;

    private Injected<IMediator> InjectedMediator { get; set; }
    private IMediator Mediator => InjectedMediator.Service;

    public void Initialize(InitializationEngine context)
    {
        if (_initialized)
        {
            return;
        }

        var contentEvents = context.Locate.ContentEvents();
        contentEvents.SavedContent += OnSavedContent;

        _initialized = true;
    }

    private void OnSavedContent(object sender, ContentEventArgs contentEventArgs)
    {
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

So now there is an event handler and we somehow should call the Mediator. To start with it, we have to create our own event types. Here is an example of the SavedContentEvent.

public class SavedContentEvent : INotification
{
    public SavedContentEvent(ContentReference contentLink, IContent content)
    {
        ContentLink = contentLink;
        Content = content;
    }

    public ContentReference ContentLink { get; set; }

    public IContent Content { get; set; }
}

This event contains only those properties which are important for this event and not more.

Now we are ready to publish our first event. Locate mediator instance, create our event from the ContentEventArgs and call a mediator's Publish method with our event as a parameter.

private Injected<IMediator> InjectedMediator { get; set; }
private IMediator Mediator => InjectedMediator.Service;

private void OnSavedContent(object sender, ContentEventArgs contentEventArgs)
{
    var ev = new SavedContentEvent(contentEventArgs.ContentLink, contentEventArgs.Content);
    Mediator.Publish(ev);
}

The last step for the mediator to be able to publish events is its configuration. You can find configuration examples for different IoC containers in the documentation. Here is an example of the configuration required for StructureMap which is added in the configurable initialization module.

container.Scan(
    scanner =>
    {
        scanner.TheCallingAssembly();
        scanner.AssemblyContainingType<IMediator>();
        scanner.WithDefaultConventions();
        scanner.ConnectImplementationsToTypesClosing(typeof(IRequestHandler<,>));
        scanner.ConnectImplementationsToTypesClosing(typeof(IAsyncRequestHandler<,>));
        scanner.ConnectImplementationsToTypesClosing(typeof(ICancellableAsyncRequestHandler<>));
        scanner.ConnectImplementationsToTypesClosing(typeof(INotificationHandler<>));
        scanner.ConnectImplementationsToTypesClosing(typeof(IAsyncNotificationHandler<>));
        scanner.ConnectImplementationsToTypesClosing(typeof(ICancellableAsyncNotificationHandler<>));
    });
container.For<SingleInstanceFactory>().Use<SingleInstanceFactory>(ctx => t => ctx.GetInstance(t));
container.For<MultiInstanceFactory>().Use<MultiInstanceFactory>(ctx => t => ctx.GetAllInstances(t));

Now when everything is set up, how to use these published events? You have to create handlers for the events. The meditator will send events to all event handlers. So you can create as much event handlers as you need for the single event. Event handlers support Dependency Injection, so you can inject whatever services you need in the constructor. Here is an example how handlers for the SavedContentEvent could look like.

public class SendAdminEmailOnSavedContent : INotificationHandler<SavedContentEvent>
{
    private readonly IEmailService _emailService;

    public SendAdminEmailOnSavedContent(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void Handle(SavedContentEvent notification)
    {
        // Handle event.
    }
}

public class LogOnSavedContent: INotificationHandler<SavedContentEvent>
{
    public void Handle(SavedContentEvent notification)
    {
        // Handle event.
    }
}

Summary

This solution might look too complex for handling some simple events but usually, those simple events become quite complex in our applications. And then event handling for all cases of those are baked in the initialization module's single method. The code becomes hard to maintain.

With a mediator, events have separate handlers for each case you need. So it is much easier to change the code when requirements change. It is much easier to add new event handling for new requirements and in general, the code becomes much easier to reason about.