Here’s part seven of (at least) 10 Advanced Windsor Tricks.
Facilities are Windsor’s main extension method. Pretty much all the bolt-on functionality that Windsor supports is via facilities and we’ve already seen a few in previous tricks. Facilities are easy to write, they basically just give you an opportunity to do some additional configuration and registration and the MicroKernel is extensible enough to support some very sophisticated scenarios.
For my example I want to take an idea I broached in the previous trick, a convention based event wiring facility. But first, in order to write a convention based event wiring facility I need some conventions:
- Events have strongly typed signatures, so no EventHandler. This facility is not designed for wiring up button click events, but is instead designed for strongly typed message publishing.
- A subscriber announces, via configuration, that it’s interested in a particular event type. The facility will wire it up to any event of the given type.
- A publisher should not have to have any special configuration. Simply having a public event will allow the facility to match it to subscribers.
Let’s start our facility. We can call it ‘EventSubscriptionFacility’. To act as a facility it has to implement IFacility:
public class EventSubscriptionFacility : IFacility { public void Init(IKernel kernel, IConfiguration facilityConfig) { throw new NotImplementedException(); } public void Terminate() { throw new NotImplementedException(); } }
A facility simply gives you a chance to do whatever registration work you need in its ‘Init’ method with the given kernel and configuration. We’ll see later how we can hook up to various kernel events to implement the functionality we need.
But before we start work on our facility, let’s write a test for it. First we need a subscriber and a publisher:
public class TypedMessagePublisher : IStartable { public event Action<NewCustomerEvent> NewCustomer; public void CreateNewCustomer(string name, int age) { if (NewCustomer != null) { NewCustomer(new NewCustomerEvent { Name = name, Age = age }); } } public void Start(){} public void Stop(){} } public class NewCustomerSubscriber { public bool NewCustomerHandled { get; private set; } public string CustomerName { get; private set; } public int CustomerAge { get; private set; } public void Handle(NewCustomerEvent newCustomerEvent) { NewCustomerHandled = true; CustomerName = newCustomerEvent.Name; CustomerAge = newCustomerEvent.Age; } } public class NewCustomerEvent { public string Name { get; set; } public int Age { get; set; } }
Our publisher publishes an event Action<NewCustomerEvent> which we want our subcriber to subscribe to. As in the previous trick, we make our publisher implement IStartable so that it get resolved for the first time during registration. Here’s a test to exercise our new facility:
var container = new WindsorContainer() .AddFacility<StartableFacility>() .AddFacility<EventSubscriptionFacility>() .Register( Component.For<TypedMessagePublisher>(), Component.For<NewCustomerSubscriber>() .SubscribesTo().Event<Action<NewCustomerEvent>>().WithMethod(s => s.Handle) ); var subscriber = container.Resolve<NewCustomerSubscriber>(); Assert.That(!subscriber.NewCustomerWasHandled); var publisher = container.Resolve<TypedMessagePublisher>(); publisher.CreateNewCustomer("Sim", 42); Assert.That(subscriber.NewCustomerWasHandled, "NewCustomerEvent was not handled"); Assert.That(subscriber.CustomerName, Is.EqualTo("Sim")); Assert.That(subscriber.CustomerAge, Is.EqualTo(42));
As you can see there’s nothing explicitly linking the subscriber to the publisher. The publisher needs no extra configuration and the subscriber is merely configured to describe the event type it cares about and which method it wants to use to handle it.
Here’s the fluent API that allows us to do the subscription. It’s pretty standard stuff and mostly just generic type wrangling noise:
public static class EventSubscriptionFacilityConfigurationExtensions { public static SubscriptionRegistration<TComponent> SubscribesTo<TComponent>( this ComponentRegistration<TComponent> registration) { return new SubscriptionRegistration<TComponent>(registration); } } public class SubscriptionRegistration<TComponent> { private readonly ComponentRegistration<TComponent> componentRegistration; public SubscriptionRegistration(ComponentRegistration<TComponent> componentRegistration) { this.componentRegistration = componentRegistration; } public SubscriptionRegistrationForMethod<TComponent, TEvent> Event<TEvent>() { return new SubscriptionRegistrationForMethod<TComponent, TEvent>(componentRegistration); } } public class SubscriptionRegistrationForMethod<TComponent, TEvent> { private readonly ComponentRegistration<TComponent> componentRegistration; public SubscriptionRegistrationForMethod(ComponentRegistration<TComponent> componentRegistration) { this.componentRegistration = componentRegistration; } public ComponentRegistration<TComponent> WithMethod(Func<TComponent, TEvent> handlerProvider) { componentRegistration.ExtendedProperties(Property .ForKey(EventSubscriptionFacility.SubscriptionPropertyKey) .Eq(new EventSubscriptionFacilityConfig(typeof(TEvent), handlerProvider))); return componentRegistration; } } public class EventSubscriptionFacilityConfig { public Type EventType { get; private set; } public Delegate HandlerProvider { get; private set; } public EventSubscriptionFacilityConfig(Type eventType, Delegate handlerProvider) { EventType = eventType; HandlerProvider = handlerProvider; } }
The neatest trick here is the ‘WithMethod’ method that allows us to grab a delegate function that takes an instance of our subscriber and returns an event handler delegate. We store that function and the event type in an extended property of the componentRegistration. Extended properties are very useful when we want to configure a registration in some way and then retrieve later in a facility.
Now to the facility itself. The core trick here is simply to attach handlers to the kernel’s ComponentModelCreated and ComponentCreated events. ComponentModelCreated is fired when the configuration is consumed by the container and the model the container uses for each component is created. This is where we find our subscribers and extract the configuration we stored earlier. That configuration is saved in a dictionary.
The ComponentCreated event is triggered when an instance of a component is created. This usually happens on the first ‘Resolve’ call for a singleton, but in our case our publishers are marked with IStartable so they will be created during registration. Here we find any component that implements an event and then lookup any subscribers than have registered to handle that event. Then it’s a simple matter of getting the handler using the function we stored earlier and registering it with the event.
public class EventSubscriptionFacility : IFacility { public const string SubscriptionPropertyKey = "__event_subscription__"; private readonly IDictionary<Type, EventSubscriptionInfo> subscriptionConfigs = new Dictionary<Type, EventSubscriptionInfo>(); private IKernel kernel; public void Init(IKernel kernel, IConfiguration facilityConfig) { this.kernel = kernel; kernel.ComponentModelCreated += KernelOnComponentModelCreated; kernel.ComponentCreated += KernelOnComponentCreated; } private void KernelOnComponentModelCreated(ComponentModel model) { if(model.ExtendedProperties.Contains(SubscriptionPropertyKey)) { var subscriptionConfig = (EventSubscriptionFacilityConfig) model.ExtendedProperties[SubscriptionPropertyKey]; subscriptionConfigs.Add(subscriptionConfig.EventType, new EventSubscriptionInfo(model.Name, model.Service, subscriptionConfig.HandlerProvider)); } } private void KernelOnComponentCreated(ComponentModel model, object instance) { foreach (var eventInfo in model.Implementation.GetEvents()) { if (subscriptionConfigs.ContainsKey(eventInfo.EventHandlerType)) { var eventSubscriptionInfo = subscriptionConfigs[eventInfo.EventHandlerType]; var subscriber = kernel.Resolve( eventSubscriptionInfo.ComponentId, eventSubscriptionInfo.ServiceType); var handler = (Delegate)eventSubscriptionInfo.HandlerProvider.DynamicInvoke(subscriber); eventInfo.AddEventHandler(instance, handler); } } } public void Terminate() { } } public class EventSubscriptionInfo { public string ComponentId { get; private set; } public Type ServiceType { get; private set; } public Delegate HandlerProvider { get; private set; } public EventSubscriptionInfo(string componentId, Type serviceType, Delegate handlerProvider) { ComponentId = componentId; ServiceType = serviceType; HandlerProvider = handlerProvider; } }
Now our test will pass.
It’s a limited first attempt, please don’t copy it and use in production. For example, it won’t work if a component has multiple subscriptions, or indeed if multiple subscribers are interested in the same event type, but I’m sure it would only take a little more effort to correct that.
The patterns I’ve shown here are pretty standard configuration and facility patterns that you see repeated throughout the Windsor code base. You can use a facility to something as simple as configuring a few components that you need to repeatedly register in multiple projects, or something as complex as the WCF Facility that we’ll be looking at soon.
Nice quick-and-dirty implementation.
ReplyDeleteDo you need to ensure that Release is called for the resolved components? How would you approach extending this to do that?