Freigeben über


In which I learn that MEF Composes Parts, not ExportDefinitions

[Rewrite - 06/04/10]

Here’s a little MEF sample (minus definition of TestMetadataFilteringCatalog) that I wrote, (not intending it to be a sample):

Its operation confused the heck out of me. I couldn’t understand why both FooTest and BarTest were being populated in p.Actions. Because function Filter() should only match exports with metadata: Category == Category.Y.

I started looking at MEF because we are thinking of using the composition scheme in a new project. While reading more about it, I came across a post by Glenn Block on MSDN forums. A link. The bit that intrigued me when I found this is

“In addition to just finding out what is available, MEF exports carry additional metadata. I can use this metadata to query the container for the types of things it has available with a fine level of granularity. I can do this without ever manufacturing any instances (delay loaded). In my viewer case, I can ask the container to give me just the metadata for the IFileViewers. Then I can loop through this to populate the file types browser. Only when someone opens a specific file type will I then manufacture the actual instance. Having the explicit attribute based approach means we can do static analysis of the application. We can know whether or not the things it needs are available”

This idea of metadata which could be used for querying assemblies sounded like it could also be abused in our test code as a way of discovering and filtering a group of automated tests to execute. Hence the sample code.

class Program

{

    static bool Filter(IDictionary<string, object> metadata)

    {

        return Category.Y.Equals(metadata["Category"]);

    }

 

    static void Main(string[] args)

    {

        Program p = new Program();

 

        var inner = new AssemblyCatalog(Assembly.Load("HelloTestCases"));

        var catalog = new TestMetadataFilteringCatalog(inner, Filter);

        var container = new CompositionContainer(catalog);

        container.ComposeParts(p);

        //p.Actions has 2 guys?

    }

 

    [ImportMany(typeof(Action))]

    public List<Action> Actions { get; set; }

}

 

public class Foo

{

    [CustomExportAttr(Category = Category.X)]

    public static void FooTest()

    {

        Console.WriteLine("foo");

    }

 

    [CustomExportAttr(Category = Category.Y)]

    public static void BarTest()

    {

        Console.WriteLine("bar");

    }

}

 

‘p.Actions has two guys?’ Why? At first I couldn’t understand the problem. I felt doomed to hopeless failure.

The important thing to notice for explaining the behavior is that the Part being composed is Foo as a whole. The methods of Foo are not parts. They are just exports from the Foo part. Foo will provide either none or all of its exports at once.

I found the semantics a bit weird at first. Why? Well, let’s look at an alternative. I wanted to export functions as delegates, and import them conditionally.  Suppose I had defined things instead with

public class Foo
{
    [CustomExportAttr(Category = Category.X)]
    /*non-static*/ public Action FooTest() = () => {…} //same semantics as above, but now a field

    [CustomExportAttr(Category = Category.Y)]
    /*non-static*/ public Action BarTest() = () => {…} //same semantics as above, but now a field
}

I might have understood the behavior in this case a little more easily, because clearly once we get into non-statics, then a Foo object is a unit of composition, and both exports are provided by the same object.

But in the static case (our original code) , there is no longer a Foo object to act as the unit of composition (part) that provides both export Foo and export Bar. So what is acting as the unit of composition? It appears instead to be the Foo type.

[Note: it’s a little hard to judge - you don’t e.g. see the Foo static constructor called when composition happens. It only gets called when one of the delegates is invoked, and code inside actually needs to run.]

So again, this is why my program wasn’t working: The catalog (and MEF) is a way of choosing Parts, not exports. And the filtering I had applied was therefore filtering Parts, and not exports.

I thought hard about how to work around the problem.

First idea - Give up on MEF composition, but try to keep it for autopopulating the exports. I don’t have any complex composition happening here, so I can just query the catalog for parts directly… no wait, that won’t work. A part is still a part.

Second idea – Give up on MEF completely, and reflect it myself. But I really still want to use MEF.

Third idea – Implement my own reflecting parts catalog. Then the parts can be whatever I want them to be….  Yes. This is actually a gratuitous and wasteful solution. I even implemented it.

But finally – in the process of that gratuitous MEF solution, I learned about Lazy<T, Metadata>imports. These are pretty cool, and they get me back to a really simple MEF-y solution which works just as Glen’s post describes.

class Program

{

    static void Main(string[] args)

    {

        Program p = new Program();

 

        var inner = new AssemblyCatalog(Assembly.GetExecutingAssembly());

        var container = new CompositionContainer(inner);

        container.ComposeParts(p);

        //when evaluated, p.CategoryYActions has Count == 1 :)

        foreach (var action in p.CategoryYActions)

        {

            action.Invoke();

        }

    }

 

    [ImportMany(typeof(Action))]

    public IEnumerable<Lazy<Action,Dictionary<string,object>>> ActionsImports { get; set; }

 

    public IEnumerable<Action> CategoryYActions

    {

        get

        {

            foreach (Lazy<Action, Dictionary<string, Object>> import in ActionsImports)

            {

                if (Category.Y.Equals(import.Metadata["Category"]))

                {

                    yield return import.Value;

                }

            }

        }

    }

}

*Still much later I also learn that the class ImportDefinition theoretically also supports an arbitrary Filter function – there is just no way to declare such an arbitrary filter function in an [Import] or [ImportMany] attribute. Still, you might want to consider it in the rare case that you are implementing your own custom MEF definitions.