Friday, November 13, 2009

A Deeper Look at MEF's Imports and Exports

In the last article, I provided a very brief introduction to MEF and showed a very simple console application.  In this section, we'll be looking at the issues associated with that sample and diving into the various aspects of declaring your Imports and Exports to overcome those issues.  The code and samples in this post will still be within a single assembly.  Because of this, I'll may use the word Dependencies and Parts interchangeably.

The Issues with the Last Example's Simplicity

At the end of the last post, we looked at a simple console application that used MEF to wire our dependencies within the same assembly together.  Below is the code for our Program.cs and SimpleMessage.cs classes again.

Program.cs

   1:  using System;
   2:  using System.ComponentModel.Composition;
   3:  using System.ComponentModel.Composition.Hosting;
   4:  using System.Reflection;
   5:   
   6:  namespace MEFExample2
   7:  {
   8:      class Program
   9:      {
  10:          [Import()]
  11:          string Message { get; set; }
  12:   
  13:          static void Main(string[] args)
  14:          {
  15:              Program p = new Program();
  16:              p.Run();
  17:          }
  18:   
  19:          void Run()
  20:          {
  21:              Compose();
  22:              Console.WriteLine(Message);
  23:              Console.ReadKey();
  24:          }
  25:   
  26:          private void Compose()
  27:          {
  28:              var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
  29:              var container = new CompositionContainer(catalog);
  30:              container.ComposeParts(this);
  31:          }
  32:      }
  33:  }

SimpleMessage.cs

   1:  using System.ComponentModel.Composition;
   2:   
   3:  namespace MEFExample2
   4:  {
   5:      class SimpleMessage        
   6:      {
   7:          [Export()]
   8:          string MyMessage
   9:          {
  10:              get { return "Hello, Extensible World"; }
  11:          }
  12:      }
  13:  }

Now, what happens when we change our SimpleMessage class to include a new string property, we'll call it NewMessage, that's also decorated with the [Exprot()] attribute and run the application again?

   1:  [Export()]
   2:  string NewMessage
   3:  {
   4:      get { return "This is another Message."; }
   5:  }

Example2-ChangeRejectedException

Doh! Runtime Error of Type ChangeRejectedException. At this point MEF doesn't know how to wire up our dependencies properly so we'll need to help it out.  First, let's look at what the purpose of Imports and Exports are and how we can use them better.

Defining the Extensibility Contract Using Text Descriptions.

When MEF is composing the various parts of our application together, it attempts to match the members we declare as Exports to matching Imports.  In our initial example, there was only a single Export and Import that were of a matching type (string) and the mapping was easy.  Once we added the second property of the same type, MEF didn't know what to do and attempted to apply both string Exports to our only string Import.  What if we only wanted the SimpleMessage.MyMessage property to be mapped to Program.Message property?  In order to to this, MEF provides options for the Import and Export attributes that allow us to declare a specific label and/or type that can be used in the mapping process.

In order to use a label to declare the mapping, all we would have to do is change the Program.Message  and SimpleMessage.MyMessage declarations to the following:

   1:  public class Program
   2:  {
   3:      [Import("ExampleMessage")]
   4:      string Message { get; set; }
   5:   
   6:      ...
   7:  }
 
   1:  public class SimpleMessage        
   2:  {
   3:      [Export("ExampleMessage")]
   4:      public string MyMessage
   5:      {
   6:          get { return "Hello, Extensible World"; }
   7:      }
   8:   
   9:      ...
  10:  }

As you can see, we provided a simple text description/label to the Import and Export attributes that allow us better control of the mapping.  By making just this update, we can now compile and run the code successfully.

Defining the Extensibility Contract Using Types

For simple cases like this, just passing a text description or label is more than enough to handle the mappings.  But I also said that we could use Types in addition (or in place of) the descriptions to handle the mappings.  In the original example from the last post, we inferred this aspect since the properties were the same type (both were string); however, we can modify our code so that Program.Message took an object instead of just a string.

To implement this change, let's make some changes to our application.  First, let's define a new Interface called IMessage that defines the property MyMessage and have our SimpleMessage class implement it.  Next, we need to make some changes to our Export() declaration.  Since we are now passing types we will want to move the Export() decoration to be class level instead of on the SimpleMessage.MyMessage property and also explicitly declare the Export to be of our IMessage type as shown below.

   1:  [Export(typeof(IMessage))]
   2:  public class SimpleMessage : IMessage      
   3:  {
   4:      public string MyMessage
   5:      {
   6:          get { return "Hello, Extensible World"; }
   7:      }
   8:   
   9:      ...
  10:  }

Lastly, we need to update the code in our Program class by changing three things.  We need to change the Message property to be of type IMessage instead of string.  We should also change the Import() decoration on our property to explicitly expect the IMessage type. And finally, we need to change our output to Message.MyMessage due to the type change.  The updated Program.cs code is shown below.

   1:  using System;
   2:  using System.ComponentModel.Composition;
   3:  using System.ComponentModel.Composition.Hosting;
   4:  using System.Reflection;
   5:   
   6:  namespace MEFExample2
   7:  {
   8:      public class Program
   9:      {
  10:          [Import(typeof(IMessage))]
  11:          IMessage Message { get; set; }
  12:   
  13:          static void Main(string[] args)
  14:          {
  15:              Program p = new Program();
  16:              p.Run();
  17:          }
  18:   
  19:          void Run()
  20:          {
  21:              Compose();
  22:              Console.WriteLine(Message.MyMessage);
  23:              Console.ReadKey();
  24:          }
  25:   
  26:          private void Compose()
  27:          {
  28:              var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
  29:              var container = new CompositionContainer(catalog);
  30:              container.ComposeParts(this);
  31:          }
  32:      }
  33:  }

Now when we run the application, the mappings are still correct and we now have a type specific mapping based on an interface.  In addition to explicitly declaring the type of the mapping, we can also add a label to removing any additional ambiguity if we have multiple classes that implement our IMessage interface but only want to import a specific one.  Though, what if we DID want to import more than one instance of classes that implement our IMessage interface?  Thankfully, MEF has a way for that as well.

Adding a Second Message with ImportMany()

If you can remember the beginning of this specific post, we took our SimpleMessage class and added a second string property which caused our mappings to break since MEF didn't know which string property to map to our then, Program.Message string property. With this situation we addressed it through the use of explicitly declaring the mapping through labels and type abstractions; however, there is another way.  Building on the updates from the last section, what if we were to create a new class that implements our IMessage interface and decorates itself with the Export() attribute?  Let's add a new class to our project called SecondMessage.cs and implement that scenario.  Below is the SecondMessage.cs code:

   1:  using System.ComponentModel.Composition;
   2:   
   3:  namespace MEFExample2
   4:  {
   5:      [Export(typeof(IMessage))]
   6:      public class SecondMessage : IMessage
   7:      {
   8:          public string MyMessage
   9:          {
  10:              get { return "Hello, From a Second Class"; }
  11:          }
  12:      }
  13:  }

When we attempt to run the application, we get the same ChangeRejectedExcemption that we did when we had 2 string exports before.  Instead of doing all of the explicit stuff we did previously, let's change our expectations of the number of objects we're importing into our Program.Message property.  MEF addresses this issue by providing an ImportMany() attribute. ImportMany() allows MEF to map many different objects that conform to the contract (label and/or type) to a single property, where as Import() is only a singular mapping.  Since we are now working with multiple instances of IMessage though, we need to update our property to be a collection, as well as update our output to loop through the results.  After applying these updates, Program.cs should resemble the below code.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel.Composition;
   4:  using System.ComponentModel.Composition.Hosting;
   5:  using System.Reflection;
   6:   
   7:  namespace MEFExample2
   8:  {
   9:      public class Program
  10:      {
  11:          [ImportMany(typeof(IMessage))]
  12:          IEnumerable<IMessage> Messages { get; set; }
  13:   
  14:          static void Main(string[] args)
  15:          {
  16:              Program p = new Program();
  17:              p.Run();
  18:          }
  19:   
  20:          void Run()
  21:          {
  22:              Compose();
  23:   
  24:              foreach(IMessage message in Messages)
  25:              {
  26:                  Console.WriteLine(message.MyMessage);
  27:              }
  28:   
  29:              Console.ReadKey();
  30:          }
  31:   
  32:          private void Compose()
  33:          {
  34:              var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
  35:              var container = new CompositionContainer(catalog);
  36:              container.ComposeParts(this);
  37:          }
  38:      }
  39:  }

Running our application after these latest updates, we see that we get both messages as we expected.  In the next post, we'll take a quick look at how we can manage this need for looping by using LINQ; however, for now,we can see how to we're able to better manage our mappings using Imports and Exports of types and/or labels as well as working with multiple imports.

Resources


kick it on DotNetKicks.comShout it

3 comments:

  1. Very nice explanation. Author read the mind of reader and explained all the things explicitly. Thanks :)

    ReplyDelete