Wednesday, June 11, 2008

MAF Gymnastics: Event Hub

Download the Solution

The first problem I will attempt to address in this series is that of providing an event aggregator service (which I call an event hub, just to save on typing). That is, how can we allow add-ins to subscribe to, and publish events on a many-to-many event bus that is managed and implemented by the host?

When using MAF, there are a number of problems that must be solved in order to facilitate such a service. For one, how can we present a decent API to the add-in developer? The lack of generics support in MAF makes this difficult (see my previous post for details), and limits the overall effectiveness of the solution.

Next up, how can we ensure that event subject types (the CLR types that contain event payloads) do not leak into the host? If two add-ins communicate with a custom subject type, that type should not be loaded into the host's AppDomain. It should only be loaded into the AppDomain of those add-ins that care about that particular event.

With that in mind, how can we allow the host to use the event hub in much the same way as add-ins? That is, how can we design the host views such that the host can remain agnostic of subject types where necessary, but can publish and subscribe on the event hub where desired?

Finally, we'll discuss how the event hub service is exposed to add-ins. That is, how does an add-in actually obtain an instance of the event hub service.

The API

The contracts for the event hub service looks like this:

image

Importantly, you can see that there is nothing in these APIs that will cause types to leak from the add-in AppDomains to the host (or vice-versa). A string is used to identify subjects as opposed to a CLR type or RuntimeTypeHandle, both of which will result in errors because the corresponding subject type may not be available to the host. Also, subjects themselves are passed around as a byte array rather than an object instance. That allows the host to remain completely unaware of the actual CLR types involved.

The views for the event hub service look slightly different depending on whether you're an add-in or the host. For an add-in, the API looks like this:

image

The ISubscriber interface is what event subscribers must implement. It has a single method that is called when a relevant event is published on the event bus. The IEventHubService interface allows events to be published, and subscribers to be registered and unregistered.

A CLR type is used to scope events. That is, subscribers register their interest in events with a certain subject type. Subscribers will only receive events of types they have registered their interest in. In reality, you might want add-ins to just use a string to identify events. However, I wanted something that demonstrates how to differ your view from your contract.

This API is fairly clean and simple, but suffers from a lack of generics. Ideally, we would be able to define a generic ISubscriber<T> interface and have subscribers receive a strongly-typed subject argument. However, whilst the MAF pipeline does allow generic members in non-generic types, it does not allow generic types. That's because it must automatically hook up contracts and views with their corresponding adapters. If the contract or view is a generic type, MAF is not able to close the type (presumably because it does not have enough information to do so).

The API for the host is a little more complicated than that for the add-in:

image

The reason for the complication is that we want the event hub to be usable from the host in much the same way as it is from an add-in. To that end, we differentiate between two different subscribers: add-in subscribers and the host itself. There are basically two "modes" that the host-side works in: type-agnostic (add-in subscriber) and type-aware (host subscriber). The type-aware mode is not strictly necessary for the host to work with the event hub, but it sure makes it easier and consistent with the way the event hub works for add-ins.

The finally part of the API I want to draw your attention to is the IHost interface:

image

The EventHubService property on this interface is the means by which add-ins actually gain access to the service. In a later post I will discuss a more generalized and extensible way of exposing services to add-ins.

Adapters

The key to this solution is contained within the adapters. The adapters ensure that the host remains blissfully unaware of subject types and payloads. Consider this add-in code:

host.EventHubService.Publish(new CustomEventSubject() { Message = "Giggidy giggidy!" });

The add-in side view-to-contract adapter adapts this call such that the subject type (CustomEventSubject) is converted to a string used to identify the event, and the subject itself (the CustomEventSubject instance) becomes a byte array via standard .NET serialization. Neither parameter makes it unscathed into the host's AppDomain, which is fortunate because that would cause problems where the subject type was unknown to the host. The contract-to-view adapter does the opposite: it resolves a .NET Type instance based on the identifying string, and deserializes the byte array into an instance of the subject type.

If little alarm bells are going off inside your head right now after hearing about the automatic (de)serialization, it’s for good reason. I discuss why you should use the event hub service with care below.

Host

The host is largely untouched with respect to the skeletal solution. The one change is the use of qualification data to dictate the load order for add-ins. We need the subscriber add-in to load prior to the publisher add-in, otherwise the event publication will occur before there is a subscriber!

Each add-in has a QualificationDataAttribute applied to it to specify the load order. The host then uses the value in this attribute to order the add-ins prior to activation:

var orderedTokens = from token in tokens
                    orderby int.Parse(token.QualificationData[AddInSegmentType.AddIn]["LoadOrder"])
                    select token;

Note that this is just a simple fix for this specific scenario. A more real-world solution would be to allow one add-in to depend on another and order the activations accordingly. Perhaps I’ll address this in a later post.

When you execute the host, you should see something like this:

image

As you can see from the output, both the add-ins and the host are utilizing the event hub service. The add-ins are communicating with string subjects, and with a custom subject.

Deployment

Again, deployment hasn't really changed much compared to the skeletal solution. The one big difference is the Subjects project. This project contains a subject type that the add-ins use to communicate. Since both add-ins require access to this type, it must be referenced by each add-in and loaded into their AppDomain. Therefore, the build script for each add-in project copies this file to the add-in’s directory:

<Copy SourceFiles="$(OutputPath)\PublisherAddIn.dll;$(OutputPath)\Subjects.dll" DestinationFolder="..\Host\$(OutputPath)\AddIns\PublisherAddIn" />

Performance

Notably missing from the APIs is a filtering mechanism. That is, a way for publishers or subscribers to filter their interest in a particular event. I excluded this for two reasons:

  1. It was too much effort for the purposes of this post, and would complicate things quite radically.
  2. It would encourage cross AppDomain filtering, which is a potential performance issue.

Any event published to the event hub service has the potential to visit many AppDomains, so it should be used with care and only when necessary. If your add-in requires an event hub service, but it does not need to cross the AppDomain boundary (ie. it is internal to your add-in) you might instead consider something like this. To reiterate, the service discussed in this post should be used only when inter-add-in communication is required. Don’t use it for intra-add-in communications.

A Word of Warning

One thing you must be aware of before using this service is that it circumvents some of the measures MAF takes to protect you from versioning issues. Consider what would happen if you had a custom subject type that was consumed (subscribed to) by many add-ins. Suppose you wanted to alter that custom subject type and those alterations would break existing add-ins. You don’t have the luxury of using adapters to help out because the subject was never explicitly called out in the contracts. Instead, the best you can do is use built-in .NET serialization mechanisms (for example, OptionalFieldAttribute) to provide some compatibility, but this will be limited when compared to using a MAF pipeline.

Because of this, you want to be sure that any custom subject types you create are resilient to change. And when they must change, be sure to ensure that changes are backwards compatible (write some unit tests to verify). And if you simply cannot preserve compatibility, consider defining a new subject type altogether because the alternative is requiring every add-in be recompiled against the new version of the subject type.

Conclusion

An event hub service is imperative for applications where add-ins are required to communicate with each other in a loosely-coupled fashion. The solution I presented in this post allows you to achieve that even when add-ins are in completely separate AppDomains (or even processes) and the host has no knowledge of the types being used to communicate between the add-ins.

Limitations of MAF prevent us from providing an ideal solution - one that leverages generics to provide a strongly-typed API. However, this solution is still very workable. Just be cautious when defining subject types - you don't want them to become a versioning headache, especially considering MAF is designed to help prevent such headaches.

Download the Solution

2 comments:

Alex said...

Hi, Kent.
Just found your topic while searching on MSDN forums, where you asking about this issue "A host application's UI services, such as resource inheritance, data binding, and commanding, are not automatically available to add-in UIs. To provide these services to the add-in, you need to update the pipeline."

May be you can show me how to implement resource inheritance to be availiable to add-ins?

Kent Boogaart said...

Hey Alex,

WPF integration is definitely on my list of things to cover. I'm not convinced that bridging the WPF logical tree is actually possible in MAF, but I'm yet to investigate fully.

Best,
Kent