I decided to start a series of blog posts which would cover some of my favourite patterns and practices in writing good code. The opening post will be about on of my favourite patterns, event aggregator, also known as Publish-Subscribe, Event Publisher, etc.
Event Aggregator pattern is well known in the enterprise community, but i still see a lot of solutions which completely ignore this fantastic pattern and rather use conventional hooks or events. I’d say that the event aggregator pattern is one of the most powerful ones to supercharge your conventional CRUD applications. I wanted to use this post to reiterate on the positive experience and to have one more point of reference for this great pattern.
The traditional Business logic implementation
The traditional way of implementing Business logic implementation is a rather linear way of doing it. You have your BusinessService, which encapsulates your standard repository.If you would like to plug into it, you would either use a partial function or an abstract function or a conventional event delegate.
public class BusinessService { public void DoSmth() { //.. business logic AfterDoSmthVirt(); AfterDoSmthPart(); AfterDoSmthEvent(); } public virtual void AfterDoSmthVirt() { } public partial void AfterdoSmthPart(); public void AfterDoSmthEvent() { If (AfterDoSmthDelegate!=null) AfterDosmthDelegate(this, new EventArgs()); } }
The good thing about it is that it’s very straightforward, it’s flexible to an extend (you can have whatever paremeters you want), and there’s not much plumbing around it. This pattern is very popular, i’ve seen it implemented a number of times, especially in generated code. Much more often than the event aggregator pattern, especially in older code.
The event aggregator
Event aggregator pattern is primarily used in UI logic, and it’s used for decoupling different parts of UI modules but still having them react to events. The same thing can be used for Business logic.
public class BusinessService { public void DoSmth() { //.. business logic EventAggregator.Inst.Publish(new SomethingDone()); } }
Firing an event aggregator looks similar to the hooks in previous example. Let’s have a look at the classic event aggregator message consumption. First let’s implement a handler class.
public class SomethingDoneHandler: IHandleEvent<SomethingDone> { public SomethingDoneHandler(params....) { } public void HandleEvent(SomethingDone smDone) { //... logic here } }
One thing left to do is register the class in the event aggregator
EventAggregator.Subscribe(new SomethingDoneHandler);
For the sake of brevity and keeping things simple and clear, the event aggregator is implemented as a singleton and it keeps a hard reference to the subscribers. There are many different ways to implement both handlers, messages and aggregator, depending on your needs and preferences.
Why event aggregator?
There are a few key features which make aggregator the optimal choice.
– modularity – hooks are usually hard coded and the only way to make different hooks for different configs is using precompilation tags. On the other hand, handlers are registered on startup and can be configured on runtime
– extensibility – while introducing new services or components to hooks can be hard (ie. the arguments of your event), or you would have to use static factories, handlers can be extended with all the services you have in your IOC container without changing the signature of the message.
– lifetime management – hooks are just method extensions, so their lifetime is usually tied to the life of the calling method. handlers can have a lifetime which spans on multiple calls, depending on your needs. They can even be singletons and react to many different events. But be careful with that:).
But what really makes the pattern shine for me is the centralization of handling the events compared to distributed model of hooks. Why is that valuable? Chaining control.
Handling events centrally makes it very easy to control what gets fired when.
For instance,say you have 2 handlers which fire on saving two different objects but which save the other object.
public class ObjectASavedHandler: IHandleEvent<ObjectASaved> { IRepository _rep, public ObjectASavedHandler(IRepository rep, params....) { _rep=rep; } public void HandleEvent(ObjectASaved message) { var objB=message.Entity.ObjectB; //do something with objectB _rep.Save(objB); } } public class ObjectBSavedHandler: IHandleEvent<ObjectBSaved> { IRepository _rep, public ObjectBSavedHandler(IRepository rep, params....) { _rep=rep; } public void HandleEvent(ObjectASaved message) { var objA=message.Entity.ObjectA; //do something with objA _rep.Save(objA); } }
What you get here happening is infinite hook firing, and to handle it properly, the hooked method has to be aware of it’s context, so you basically have to set some static bool somewhere to ignore the repeated calls, which is very messy.
When using event aggregation , it’s pretty straightforward. We can extend the handler interface with Disable chaining property, have an Event Aggregator test the property, and just ignore repeated calls to handlers if the variable is set.
That would be a basic approach, but the event aggregator could be extended in so many different ways.
One handler for related events
One other pretty awesome feature is using inheritance for having your handler respond to many different related events.
First, let’s have a look at the handler interface, the one that has to be implemented by handler classes:
public interface IHandleEvent<in TEvent> { void HandleEvent(TEvent event); }
This is very important. By using contravariance, we are enabling this crucial feature and letting the handler react to the base event and all other inherited events!
Lets have one base event which is fired whenever a database operation is executed
public class EntityDbEvent<TEntity>: IEvent { public TEntity BaseEntity {get;set;} }
Now let’s have different event for deletes and upserts (insert and update), because they usually have a bit different handling
public class EntityDeletedEvent<TEntity>: EntityDbEvent<TEvent> { } public class EntityUpsertEvent<TEntity>: EntityDbEvent<TEvent> { }
And finally lets separate code for Insert and Update
public class EntityInsertedEvent<TEntity>: EntityUpsertEvent<TEvent> { } public class EntityUpdatedEvent<TEntity>: EntityUpsertEvent<TEvent> { }
What we have here now is a classic event inheritance tree.
If for instance we subscribe a logger to EntityDbEvent, the handler would be fired on all the inherited events (for instance EntityInsertedEvent), which is incredibly powerful. We can use one handler for Insert and update event which i regularly use. And the handler for Deleted event would fire only for deleted event.
With this kind of flexibility, using aggregator with a typical CRUD application can solve 95% of Business application scenarios, including multi-client configurations, workflow control and extensibility. It’s definitely a crucial tool in every developers toolbelt.
For reference i’ll just paste some code from one of my event aggregators.
In this code example Aggregator is called EventPublisher (somewhere it’s called PubSub, Publisher etc.), Event is called Message, and IHandleEvent is renamed to IHandleMsg. It supports weak and strong reference to subscribers (i keep them in separate lists, mostly because of type safety), and it also supports blocking chain calls.
Till next time, cheers!
/// <summary> /// Enables loosely-coupled publication of and subscription to events. /// </summary> public class EventPublisher { private static EventPublisher inst; public static EventPublisher Inst { get { return inst ?? (inst = new EventPublisher()); } } readonly List<WeakReference> _subscribers = new List<WeakReference>(); readonly List<object> _permaSubscribers = new List<object>(); private bool _isHandlingLocked; /// <summary> /// Subscribes an instance to all events declared through implementations of <see cref="IHandleMsgMsg{TMessage}"/> /// </summary> /// <param name="instance">The instance to subscribe for event publication.</param> /// <param name="isStrong"></param> public void Subscribe(object instance, bool isStrong = false) { if (!isStrong) lock (_subscribers) { if (_subscribers.Any(reference => reference.Target == instance) || _permaSubscribers.Any(reference => reference == instance)) return; _subscribers.Add(new WeakReference(instance)); } else lock (_permaSubscribers) { if (_subscribers.Any(reference => reference.Target == instance) || _permaSubscribers.Any(reference => reference == instance)) return; _permaSubscribers.Add(instance); } } /// <summary> /// Unsubscribes the instance from all events. /// </summary> /// <param name="instance">The instance to unsubscribe.</param> public void Unsubscribe(object instance) { lock (_subscribers) { var found = _subscribers .FirstOrDefault(reference => reference.Target == instance); if (found != null) _subscribers.Remove(found); } lock (_permaSubscribers) { var found = _permaSubscribers .FirstOrDefault(reference => reference == instance); if (found != null) _permaSubscribers.Remove(found); } } /// <summary> /// Publishes a message. /// </summary> /// <typeparam name="TMessage">The type of message being published.</typeparam> /// <param name="message">The message instance.</param> public void Publish<TMessage>(TMessage message) { WeakReference[] toNotify; lock (_subscribers) toNotify = _subscribers.ToArray(); var dead = new List<WeakReference>(); foreach (var reference in toNotify) { var target = reference.Target as IHandleMsg<TMessage>; if (target != null && !_isHandlingLocked) { if (!target.CanChainMessages) _isHandlingLocked = true; target.HandleMsg(message); _isHandlingLocked = false; } else if (!reference.IsAlive) dead.Add(reference); } if (dead.Count > 0) { lock (_subscribers) { foreach (var weakReference in dead) _subscribers.Remove(weakReference); } } object[] toNotifyPerm; lock (_permaSubscribers) toNotifyPerm = _permaSubscribers.ToArray(); foreach (var o in toNotifyPerm) { var target = o as IHandleMsg<TMessage>; if (target != null && !_isHandlingLocked) { try { if (!target.CanChainMessages) _isHandlingLocked = true; target.HandleMsg(message); } finally { _isHandlingLocked = false; } } } } }
Leave a Comment