I’ve shown you how to use an Application Bus to dispatch events in your application, making it easier to craft code that is loosely coupled and more maintainable. One of the enhancements I briefly mentioned was to make the event handlers execute asynchronously, thereby enabling your event handlers to run in parallel. This is quite easy to implement, as I’ll show you in this post.
Before we get started, note that the code I’m showing you here has not been used in anything resembling a production environment. While the tests I’ve performed work, there may be issues with the code. I am not responsible if this code causes horrible things to happen to you. At the point you need asynchronous event handlers, you should really consider upgrading from an in-memory Application Bus to a true Message Bus, such as NServiceBus.
Recall from last time that our concrete ApplicationBus has a Raise method for dispatching events:
public void Raise<TEvent>(TEvent @event) { var handlers = _registry.GetAllHandlersFor<TEvent>(); foreach (var handler in handlers) { try { handler.Handle(@event); } catch (Exception ex) { //TODO: Log this somewhere, but don't rethrow. The caller doesn't care! } } }
Handlers are retrieved from the handler registry, and the event is dispatched to each handler in a synchronous fashion. The event is not passed to the next handler until the current handler has finished processing it.
Also note that a handler throwing an exception does not interrupt the process; the event will still be passed to the next handler in line after the exception is caught.
Thanks to the magic of the Task Parallelization Library in .NET 4, it is almost trivial to run our event handlers in parallel. All we need to do is leverage the AsParallel extension method on our list of handlers, like so:
public void Raise<TEvent>(TEvent @event) { var handlers = _registry.GetAllHandlersFor<TEvent>(); handlers.AsParallel().ForAll(h => DispatchEventTo(h, @event)); }
One thing to note is that we’re now calling a helper method, DispatchEventTo, to actually call the handler. This is necessary in order to cleanly handle exceptions that might be thrown by the handlers:
private void DispatchEventTo<TEvent>(IHandle<TEvent> handler, TEvent @event) { try { handler.Handle(@event); } catch (Exception ex) { //TODO: Log this somewhere, but don't rethrow. The caller doesn't care! } }
The end result is exactly the same behavior we had before except that our event handlers are now executing in parallel. But there’s a catch. While they are executing in parallel, the Raise method still won’t return until all the event handlers have finished executing.
If you want to truly execute the handlers asynchronously, you need to do just a bit more work, and explicitly create new tasks to dispatch to each event handler:
public void Raise<TEvent>(TEvent @event) { var handlers = _registry.GetAllHandlersFor<TEvent>(); handlers.Select(h => Task.Factory.StartNew(() => DispatchEventTo(h, @event))).ToArray(); }
Now, the Raise method will return immediately after it starts the dispatch tasks on the task factory.
The call to the ToArray extension method is to force the LINQ expression to be enumerated; remember that LINQ queries, even LINQ to Object queries, are lazily enumerated, and no tasks will be created if you don’t force the expression to be enumerated.
Do note that if you use this second, fully asynchronous approach in an ASP.NET application, your event handlers may very well execute after the web request has ended. You must be very careful not to access anything that’s going to be destroyed with the request.
And there you have it: asynchronous event processing using an Application Bus.