Sunday, July 25, 2010

Using Hints for Generic MEF Exports

It is very common to have base classes and interfaces that use generic types to define standard behaviors across business applications. One challenge with the Managed Extensibility Framework is that it doesn't directly support generic exports and imports: in other words, there is no way to effectively do the following:

...
[ImportMany(AllowRecomposition=true)]
public IService<,>[] GenericService { get; set; }
...

While there is a GenericCatalog you can use, I wanted something a little more flexible and specific to the application.

The Example Service

Let's assume we have an example "service" interface that does something to an instance of a type. We define the interface like this:

public interface IService<T> 
{
   void DoSomething(T instance); 
}

The Locator

The goal is to have a composite application that automatically picks up services that support different types of T and a locator that easily gives us the instance we are looking for. The locator looks like this:

public interface IServiceLocator
{
    IService<T> GetServiceFor<T>();
}

This allows us to easily ask for the service, and do something with it:

var service = serviceLocator.GetServiceFor<MyClass>();
service.DoSomething(myInstance);

The problem with the locator is that we don't have a basic "generic" import for the various services, and we'd have to do a lot of dancing with reflection to parse out types as they became available in order to wire them in. In this case, I felt it was easier to come up with an intermediary class to facilitate finding the service.

Hinting Around

I call it a hint because it hints to the locator where to find the right service. The interface for a service hint looks like this:

public interface IServiceLocatorHint
{
    bool ServicesType<T>();
    IServiceInterface<T> GetServiceFor<T>();
}

As you can see, the hint has two methods. One determines whether or not the hint is capable of producing the service for a given type, and the other returns that service. Now, let's assume in a dynamically loaded module we implement the service contract for MyClass using a class called MyService. It looks like this:

[Export]
public class MyService : IService<MyClass> 
{
   void DoSomething(MyClass instance) 
   {
      Debug.WriteLine(instance.ToString());
   }
}

Notice I am exporting the service as the concrete type. Next, I build a simple hint:

[Export(typeof(IServiceLocatorHint))]
public class MyModuleHints : IServiceLocatorHint
{
    [Import]
    public MyService MyServiceInstance { get; set; }
 
    public bool ServicesType<T>()
    {
        return typeof(T).Equals(typeof(MyClass));
    }

    public IServiceInterface<T> GetServiceFor<T>()
    {
        if (ServicesType<T>())
        {
            return (IServiceInterface<T>) MyServiceInstance;
        }

        throw new NotSupportedException();
    }

}

Putting it all Together

Now that we have the service wired by MEF with all dependencies, and the hint wired as well, we can implement the locator class.

[Export(typeof(IServiceLocator))]
public class ServiceLocator : IServiceLocator
{
    [ImportMany(AllowRecomposition = true)]
    public IServiceLocatorHint[] Hints { get; set; }

    public IServiceInterface<T> GetServiceFor<T>()
    {
        var serviceHint = (from hint in Hints 
                        where hint.ServicesType<T>() 
                        select hint).FirstOrDefault();

        if (serviceHint == null)
        {
            throw new NotSupportedException();
        }

        return serviceHint.GetServiceFor<T>();
    }
}

The class is simple. As modules are loaded, the main list is recomposed to include any new hints that were found. When the user requests a service, it simply finds the hint that satisfies the type, then returns the corresponding service.

Using this flexible locator class is as simple as importing it, then asking for the service:

[Import]
public IServiceLocator Locator { get; set; }

public void ProcessClass(MyClass item)
{
   var service = Locator.GetServiceFor<MyClass>();
   service.DoSomething(item); 
}

If you have multiple services in a module, you can easily build a base class that uses a dictionary to register the instances and streamline the methods that check for support and return the instances. The power of MEF is that new services are easily discovered as plugins and extensions are loaded into the main application, and you can basically build an application around what you don't know yet, rather than having to constrain it based upon what you do know.

Jeremy Likness

2 comments:

  1. Thank you for this awesome article.
    I have to re-think what I have done with your XFit sample App. Thanks a lot.

    HK.Lee

    ReplyDelete
  2. Very nice article, do you have a sample by any chance?

    Thanks,
    Paul

    ReplyDelete