Wednesday, June 18, 2008

MAF Gymnastics: Service Provider

Download the Solution

If you’ve ever worked on a composite application, you’ll know that services are their backbone. Services allow modules in the application to collaborate in both state and behavior. Moreover, services can be swapped out easily, since the modules work against an interface and are (hopefully) ignorant of the implementation behind the interface.

This post will address how to implement a service ecosystem using MAF in a composite application. In particular, how we can do this whilst keeping the host agnostic of the services it is hosting, and providing version tolerance for the services. Whilst I focus on composite applications, the techniques here can also be applied in non-composite applications.

Requirements

Agnostic Host

One of the challenges of implementing a service provider in MAF is ensuring that the host is capable of hosting any service, not just those services it knows about at compile-time. Without this ability, it’s not really a composite application and does not permit the kind of collaboration between add-ins that such an application requires.

Of course, the types related to the services must not be loaded into the host’s AppDomain. The services need to be isolated just like any other add-in.

Version Tolerance

Consider how composite applications are often written in the real world. You have a bunch of autonomous teams, each team producing and/or consuming services. Some teams consume services provided by other teams.

Then there’s the shell (aka “host”), which is responsible for hosting these modules and allowing them to collaborate. The shell is often produced by another team altogether to those producing the modules (aka “add-ins”).

Now think about what happens when one of the service-providing teams decides they need to modify their service interface. They can potentially break all the modules that depend on that service. In other words, the consumers of that service are at the mercy of the service provider.

MAF pipelines already provide a nice solution to this problem between the host and add-ins, but wouldn’t it be nice if the service provider could version their service over time, providing backwards compatibility as desired?

Solution

Add-in Types

The contracts define two types of add-ins: an application add-in and a service add-in.

image

At this point, the difference between a service add-in and an application add-in is minimal. A service add-in provides an ID by which it will be resolved by consuming add-ins.

Both add-in contracts have a common base interface (IAddInContract) from which they inherit an Initialize() method. Notice how this method returns an instance of AddInInitResult. This is crucial to the host being able to track which add-ins are hosted in which AppDomains.

The add-in views do not have this added complexity:

image

This is good, because the add-in developer need not worry about returning anything from their Initialize() implementation. Instead, this is handled by the adapters, as we’ll see later.

The host views mirror the contracts very closely, since the host does need to know and track this extra initialization information:

image

Add-in Activation and Tracking

When the host starts up, it first finds and activates service add-ins and then application add-ins. In both cases, it remembers the contract object for the add-in (that is, the MarshalByRefObject implementing the add-in). For services, it also maps the service ID back to the AddInToken so that it can obtain the contract implementation given only the service ID. This is important, of course, and you’ll see why soon.

Note that the tracking information is only actually necessary to support services, but the host code tracks application add-ins too, since that made the logic simpler.

The Host

It’s time to look at the pipeline components for the host. Firstly, the contract:

image

We’re getting closer and closer to the nitty-gritty now (I can’t believe that’s in the dictionary). The host contract has two methods of importance on it: GetPipelineDetails() and GetServiceDetails().

The former is quite simple: it just retrieves some basic information about the MAF pipeline. This information is needed by the add-in side adapters to support services. We’ll look at this further below.

The latter is also straightforward: it retrieves the service contract for a given service ID. Recall from above that when we activate services we track some information about that service. This method just allows the caller to retrieve that information.

The host view of the host is a mirror of the contract, so I won’t bother talking about it here. The add-in view of the host, on the other hand, is quite different:

image

As you can see, add-ins get a simpler view of the host. There is a single method that they can use to resolve a service by ID. There is no nonsense about getting pipeline information or what have you. The service consumer doesn’t care about that - she just wants a service.

I made this method generic just to save a little casting in the add-ins, but in reality I haven’t decided how I’d best like to compose this interface. I’m considering having the ID automatically determined by the interface type the service implements, so add-ins don’t even need to provide an ID. However, I haven’t spiked this yet so I don’t know if it will play out exactly as I hope. Anyway, I’ll be looking at that soon for a real project.

UPDATE: It works great! In my “real” project services do not need to explicitly provide an ID. Instead, the adapters infer it based on the view type the service implements. In addition, services are not explicitly requested from the host. Instead, the adapters determine the services an add-in requires by examining its Initialize() method and injecting any dependent services automatically. This results in a really nice, low-friction add-in development experience.

The Adapters

Based on the interfaces we just saw, you might assume that there’s some magic happening in the adapters to close the gap, so to speak. And you’d be right.

What we need to do is connect up the service consumer add-in to the service add-in, which already exists in its own AppDomain. Indeed, we may need to connect up multiple consumers to the service. Those consumers might be application add-ins or even other service add-ins (the sample only demonstrates the former, but the latter should work too assuming that the services are activated in the correct order).

Consider the following diagram:

Service Provider

The black lines represent the “primary” pipeline between the host and the add-ins, be they regular application add-ins or service add-ins. In the diagram we have a single service add-in (up the top) and two consumers of that service. The two consumers are built against different versions of the service (V1 and V2) but they both need to consume whatever version of the service is loaded (assuming the appropriate pipeline adapters can be found).

The service’s interface (ISomeService) extends the core service interface provided by the host (IService). Actually, this extension occurs in both the contracts and the views. As such, the service pipeline components depend on the pipeline components provided by the host. For the purposes of the primary pipeline, only the implementation of IService is used (that is, IServiceContract is used for communication over the black pipelines).

The blue lines represent the “secondary” pipeline between the service consumers and the service. It is this pipeline that provides marshalling specific to the service implementation (ISomeService).

And it is here where we need to employ some trickery to get the job done. First though, thanks to the anonymous commenter who pointed out an easier way to do this.

Here’s how it works. The add-in side contract to view adapter for the IHost interface implements the ResolveService<T>() method we saw above. In order to do this, it needs to construct a pipeline between the caller (ie. the consuming add-in) and the service itself.

The host’s AppDomain is impartial to this whole process – it merely provides some information the adapter needs to set up the pipeline. Once the adapter has this information, it uses MAF’s ContractAdapter type to adapt between the add-in’s view of the service and the existing service instance (ie. create the pipeline represented by the blue lines above). Specifically, the ContractToViewAdapter() method is used because we’re taking an existing contract (the service instance) and adapting it to the view.

Since the ContractToViewAdapter() method is generic and we don’t know the type until runtime, we need to invoke it reflectively. It might be nicer if MAF included non-generic overloads for these methods, but the fact that they’re public means that we do not need elevated privileges to invoke them via reflection. Thus, your add-in can still be hosted in a low-trust AppDomain.

Of course, once this pipeline is constructed we don’t need to do so again for the current AppDomain (unless a different view of the same service is used from within the same AppDomain, and the code caters for that case too). Therefore, we cache this information. Subsequent calls to ResolveService<T>() won’t even leave the caller’s AppDomain, which I think is pretty cool. Of course, calling into the service itself will marshal to the service’s AppDomain as expected.

Limitations

There are a couple of things you need to be aware of with respect to this technique. First and foremost, I mentioned above that MAF can only deal with one pipeline at a time. Further to this, it can only resolve one assembly from each pipeline folder. For example, all your add-in side adapters need to be in a single assembly inside the [Pipeline]\AddInSideAdapters folder. If your add-in side adapters assembly depends on another assembly, that assembly must be in the GAC in order for MAF to resolve it. MAF will not try to resolve that assembly from the same pipeline directory from which your add-in side adapters were loaded.

The problem with this is that the service pipelines depend on the host pipeline. As a simple example of this, the ISomeServiceContract included in the download extends the host’s IServiceContract. The upshot is that the host’s pipeline must be in the GAC.

In fact, this GAC registration requirement spreads virally throughout your pipeline projects. If you look in the sample you’ll see that most pipeline assemblies are registered in the GAC when you build. As such, you’ll need local admin since gacutil requires this. Unfortunately, I don’t see a way around this given MAF’s current architecture.

It’s important to point out that installing the pipeline components in the GAC is almost certainly what you want to do for deployments. Otherwise these assemblies cannot be shared between AppDomains and performance suffers badly. It’s just a shame that it’s also required for development.

Related to this issue is the possibility of name clashes in pipeline assemblies. You may have many teams working on services, and the pipeline assemblies for those services must reside in the same folder. Therefore, if one team chooses the same assembly name as another you will get a clash. This is pretty unlikely, but it’s something to keep in mind.

Less a limitation and more a “gotcha”: notice how the service implementation (in the Service project) references the service add-in views project (Service.AddInViews) and the service consumer add-ins reference the service host views project (Service.HostViews). In MAF terminology, the consumers are “hosts” of the service. This terminology doesn’t quite fit here because the consumers aren’t explicitly activating the service. In a real application, I would probably rename the service add-in views and host views to “provider views” and “consumer views” accordingly.

The Result

If you download and execute the sample you’ll see this:

image

As unexciting as this looks at first glance, it’s actually really, really cool. Trust me. What we’re seeing here is that there are four AppDomains:

  • One for the host
  • One for the service
  • One for the first service consumer
  • One for the second service consumer

The host and its pipeline know nothing about the specific service at compile-time. That is, it has no knowledge of ISomeService, only IService. And yet, it is hosting this service. That’s a check for an agnostic host.

The first service consumer is executing against version 2 of the service. It sets a value on the service to 26 and outputs the results of calling the service’s GetTimestamp() method. When it calls this method, it passes in a custom format string of “ddd MMM yyyy”.

The second service consumer is executing against version 1 of the service. It gets the value of 26 set by the first consumer and outputs it. Then it calls the service’s GetTimestamp() method and outputs that. But what’s cool is that version 1 of the service did not allow a custom format string to be passed into this method! The adapters for the service (see the Service.HostSideAdapters_V1toV2 project) are automatically using a default format string of “dd/MM/yyyy”. This proves that the correct pipeline components have been selected and used to join the service consumers to the service – that’s a check for version tolerance.

We have achieved everything we set out to achieve here. The host is agnostic of the services it will be hosting at runtime and none of the service types leak into the host. Moreover, the pipeline between service consumers and the service itself has all the intelligence you’d expect of a MAF pipeline. It selects the correct components to ensure that the service consumer can interact with the service, even if the consumer was built against an older version of the service.

Conclusion

Composite applications need a facility to provide and share services. An effective shell for a composite application needs to be agnostic of the services it is hosting, otherwise developers leveraging the shell are hamstrung by its intrinsic assumptions about the services it will host.

MAF’s concept of pipelines to handle version tolerance are equally applicable to services as they are to the shell itself. That’s simply because of the relationship between the provider of the deliverables and its consumers. As a shell or service developer, there is some uncertainty as to how many teams are leveraging your work, and how long they need to migrate to newer versions of your deliverables. Pipelines allow you to provide backwards compatibility so that consumers are not forced to update their code bases straight away.

The solution presented in this post solves both these problems. The shell knows nothing about the exact services it will be hosting at runtime. In addition, it uses a pipeline provided by the service developer to connect the service consumers to the service.

This was a particularly difficult topic to articulate, and I’m not confident that I did so well. If I failed to convey this information adequately, feel free to let me know in the comments and I’ll do my best to rectify the situation.

Download the Solution

10 comments:

KevinKerr said...

Those are quite the gymnastics. Looks like a triple backflip twist with a perfect landing. Way to take it to the next level.

Kent Boogaart said...

:) Thanks for the commentary Kevin!

Anonymous said...

Very interesting approach Kent.

In your post you mentioned that you had to do a bit of work because MAF doesn't support hooking up a new pipeline to an already running add-in.

It looks like that is actually supported, as long as you know the directory of the pipeline store. Take a look at: http://msdn.microsoft.com/en-us/library/system.addin.pipeline.contractadapter.aspx

This might make your implementation a bit cleaner.

Kent Boogaart said...

Great tip Anon - thanks! I somehow managed to miss that helper class, even after poring over the docs for hours.

Unfortunately, I still have to invoke the helper methods via reflection because they're generic and I don't know the view type at compile time. That said, at least it's a public method I'm invoking rather than a private one, so I feel slightly less dirty for doing so.

I will try to update the post over the weekend.

Kent Boogaart said...

FYI I have updated both the post and download so that it uses ContractAdapter now. Thanks again, Kent.

dhwanilshah1 said...

First of all, thanks for posting this really cool article! I was going through the code and its execution and came across something odd - posting it here -

After the code for discovering / activating service-addins in Host\Host.cs, I used the AppDomain.CurrentDomain.GetAssemblies() and printed out all the assembly names loaded in the Host domain.

I was surprised to see that Service.Contracts.dll and other service contract's host side pipeline assemblies in the Host's app-domain.

Is there any way to suppress this? The host does not need to know re. the Service.Contracts.dll, so it should not really be loading them in its app-domain.

Adam said...

Hi Kent,

Great work, I am trying to run your example - compilation is successful, however running the application fails on the line (Host.cs:57):
OutputWarnings(AddInStore.Rebuild(_pipelineDetails.PipelineLocation));

Exception of type 'System.AddIn.MiniReflection.GenericsNotImplementedException' was thrown (no inner-exception).

Any ideas what is going on here?

Thanks for the great work!

Mattias said...

First of all, very good, interesting work.

I was wondering if you could explain how you have solved the following a bit closer, and maybe provide the code changes:
In my “real” project services do not need to explicitly provide an ID. Instead, the adapters infer it based on the view type the service implements. In addition, services are not explicitly requested from the host. Instead, the adapters determine the services an add-in requires by examining its Initialize() method and injecting any dependent services automatically. This results in a really nice, low-friction add-in development experience.

I have used, and slightly altered, your approach for a current project. Basically what I want now, but don't know how to accomplish, is for the service add-in not having to provide an id (really does not matter). The consumer add-in should not explicitly have to request a specific service, instead I want something like public IList<T> ResolveDataAddIns<T>(), from where the consumer can get a list of all services implementing a specific type, and then choose which one to subscribe to at runtime.

My consumer add-ins provides a UI, which the user can interact with. From there, the user should be able to choose among applicable services to subscribe to. I.e. the service add-ins acts as "datasource", from which the consumer add-in UI is updated with data. This has been accomplished by implementing an event in the service, that the consumer handles, and updates the UI with data contained in the eventargs.

Any help would be greatly appreciated.
Looking forward to more gymnastics ;)

Martin Wiso said...

Hello, your gymnastic with MAF is exactly what I was looking at. I am working on some kind of similar problem using MAF, but I am facing memory leaks. It seems that there are some "live" remoting object/context/channels after AddIn Shutdown so my service is consuming more and more memory. I am profiling and trying to fix it, but without any significant results. Did you also face memory leaks?

Best Regards,
Martin

Kent Boogaart said...

Sorry for the late reply here, folks. I'll do what I can to answer your questions, although I haven't touched this project in a while now.

@dhwanilshah1: That strikes me as a little odd, too. I'd have to take a close look at this to try to understand what's going on there. I don't really have the time at the moment but I might get to it later. Do let me know if you figure it out.

@Adam: that's just an exception MAF throws internally (and then catches it itself). You must have your debugger set up to break on any exception - even unhandled ones in non-user code. Either change your settings or just hit F5 to continue execution.

@Mattias: On the add-in side, the interface has a generic method whereby add-ins can request services. This generic type argument is converted into a string (fully qualified name of the type). It's really just smoke and mirrors. The add-in requests a service by type, but that type is converted to a string before crossing the isolation boundary.

The host maintains a dictionary of all service add-ins. It knows an add-in is a service add-in if it extends a particular interface (IServiceAddIn). This interface has an Id property on it (and that's all), but services don't need to implement that themselves. Instead, the add-in side adapter for the IServiceAddIn type just returns the appropriate string based on the type of the service. Determining the ID was a matter of looking for the interface that the service implements that extends my IServiceAddIn interface, and using its assembly qualified name.

Of course, each service has its own pipeline. Each service's own add-in side adapter extends the one I mentioned in the last paragraph. Think of it as a base pipeline for services that is extended by specific services.

The dependency injection is also a bit of add-in side adapter magic. The host can invoke a method that executes in the add-in's AppDomain and examines the Initialize(...) method. It looks at the types the method relies on and reports back to the host. The host then attempts to resolve those types via its cache of services. If it can, it passes them into a call to Initialize(...) the add-in. If it can't, bang!

@Martin: I haven't noticed any specific memory leak issues, but admittedly I haven't looked hard for them. If you can create a bare-bones repro using only MAF then it points towards being a MAF bug, but you should be able to prove that with a profiler.