MEF

The Managed Extensibility framework or MEF is a framework released as part of .NET 4 and Silverlight that provides a mechanism to dynamically compose an application at runtime out of loosely coupled parts and provides a variety of mechanisms to discovering these parts. You can think of this as an implementation of a plugin framework that enables plugging in functionality into a container, based on information available at runtime. The most notable example of a MEF client is the Visual Studio 2010 editor which uses MEF to plugin in 3rd party extensions into it's shell proving an easy to use programming model to customize and/or extend the behavior of the IDE.

To wrap my head around MEF and how to leverage the framework to write extensible apps,I'll be going over some of the key features and APIs in MEF over the next several posts. This is the first in the series, and covers the nuts and bolts that MEF is made up of:

Imports and Exports

Imports and Exports is how MEF discovers and marries components that provide a service with components needing a service. The discovery is based on attributes.
A type decorated with the [Export] attribute indicates to MEF that the type is available to any type that needs it's services. Similarly a type needing a service is decorated with the [Import] attribute which conveys to MEF that it is in need of some type which provides the service.

Using Concrete classes

To make things a bit concrete, let's say we have a Logger which uses a TextFormatter to format messages before they are logged.

 

 
  1. [Export]
  2. publicclass TextFormatter
  3. {
  4. publicstring Format(string str)
  5. {
  6. returnstring.Format("Formatted Message:{0}", str);
  7. }
  8. }
  9. [Export]
  10. publicclass ConsoleLogger
  11. {
  12. [Import]
  13. TextFormatter Formatter
  14. {
  15. get;
  16. set;
  17. }
  18. publicvoid Log(string str)
  19. {
  20. string formattedString = Formatter.Format(str);
  21. Console.WriteLine(formattedString);
  22. }
  23. }

The [Export] attribute on the TextFormatter class indicates that the services of this type are available to any type that needs them. The [Import] attribute on the ConsoleLogger indicates that it needs a TextFormatter in order for it to log messages.
Similarly the [Export] attribute on the ConsoleLogger indicates that it's services are available to any type that needs a ConsoleLogger.

 

 
  1. class Program
  2. {
  3. [Import]
  4. public ConsoleLogger Logger { get; set; }
  5. staticvoid Main(string[] args)
  6. {
  7. Program p = new Program();
  8. CompositionContainer container =
  9. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  10. container.ComposeParts(p);
  11. p.Logger.Log("This is a test");
  12. }
  13. }

Nowhere in the code above do you see a new TextFormatter() or new ConsoleLogger().
MEF handles the creation for you. In order to do this it needs to discover the types available so that it can satify the imports. It does this using a CompositionContainer which holds onto catalogs of various types. A catalog itself is a container holding types. In the code above we are using one type of catalog called AssemblyCatalog which wraps around a specified assembly and holds onto the types available in the assembly which exports services that can be used for satisfying imports(more on catalogs later). The main program imports the ConsoleLogger, in order to satisfy this import and have MEF create a ConsoleLogger for us we need to tell MEF to go through a composition on the instance of the Program so that it can satify it's imports. This is achieved via the call to container.Composeparts(p). At the end of this step MEF has inspected the current assembly, discovered that TextFormatter exports it's services which are needed in turn by the ConsoleLogger and injected an instance of the TextFormatter into the ConsoleLogger.
Furthermore, it then creates an instance of the ConsolLogger(since it also is an [Export]) and has injected an intance of the ConsoleLogger into the intance of the main program we created.

You might say "Why can't I just use new...?" which is a valid question, considering that everything is currently in one code file/assembly, this will become clearer as we progress further along where not all types are available in the same assembly and there is no static dependency between assemblies(via add ref).

Injecting dependencies via Constructor Injection

Imports can also be specified on constructors so that dependencies can be auto injected into a type. This is specified by using the [ImportingConstructor] attribute on the constructor of the type into which the dependency needs to be injected.

 

 
  1. [Export]
  2. publicclass TextFormatter
  3. {
  4. publicstring Format(string str)
  5. {
  6. returnstring.Format("Formatted Message:{0}", str);
  7. }
  8. }
  9. [Export]
  10. publicclass ConsoleLogger
  11. {
  12. TextFormatter _formatter;
  13. [ImportingConstructor]
  14. public ConsoleLogger(TextFormatter formatter)
  15. {
  16. _formatter = formatter;
  17. }
  18. publicvoid Log(string str)
  19. {
  20. string formattedString = _formatter.Format(str);
  21. Console.WriteLine(formattedString);
  22. }
  23. }
  24. class Program
  25. {
  26. [Import]
  27. public ConsoleLogger Logger { get; set; }
  28. staticvoid Main(string[] args)
  29. {
  30. Program p = new Program();
  31. CompositionContainer container =
  32. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  33. container.ComposeParts(p);
  34. p.Logger.Log("This is a test");
  35. }
  36. }

In the above code,the TextFormatter is specified as a constructor parameter to the ConsoleLogger. the [ImportingConstructor] attribute indicates to MEF that the TextFormatter needs to be injected into the ConsoleLogger.
As before, the ConsoleLogger has an [Export] attribute and is imported into the main Program as an Import. When the Program is composed, MEF finds the TextFormatter in the AssemblyCatalog (since it's in the current assembly) and uses that to satisfy the import for the ConsoleLogger's constructor.

Exporting types from a Factory class

In the previous examples we've specified the [Export] attribute on the type itself such as on the TextFormatter and ConsoleLogger classes, which in turn causes MEF to instantiate these types for satisfying imports. Instead, if we wanted to have a factory class in charge of creating instances of a type so that you have more control, and specify these types as the ones to use for satisfying an import, we can do something like so:

 

 
  1. class FormatterFactory
  2. {
  3. [Export]
  4. public TextFormatter Formatter
  5. {
  6. get
  7. {
  8. returnnew TextFormatter();
  9. }
  10. }
  11. }

The Formatter property of the FormatterFactory provides a valid export to MEF. If another type needs to Import a TextFormatter, MEF is prepared to instantiate the FormatterFactory and invoke the "Formatter" property in order to satisy the import.

 

 
  1. publicclass TextFormatter
  2. {
  3. publicstring Format(string str)
  4. {
  5. returnstring.Format("Formatted Message:{0}", str);
  6. }
  7. }
  8. [Export]
  9. publicclass ConsoleLogger
  10. {
  11. [Import]
  12. TextFormatter Formatter
  13. {
  14. get;
  15. set;
  16. }
  17. publicvoid Log(string str)
  18. {
  19. string formattedString = Formatter.Format(str);
  20. Console.WriteLine(formattedString);
  21. }
  22. }
  23. class Program
  24. {
  25. [Import]
  26. public ConsoleLogger Logger { get; set; }
  27. staticvoid Main(string[] args)
  28. {
  29. Program p = new Program();
  30. CompositionContainer container =
  31. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  32. container.ComposeParts(p);
  33. p.Logger.Log("This is a test");
  34. }
  35. }

Notice that the [Export] atribute is no longer present on the TextFormatter, and the import in ConsoleLogger is satisfied by the FormatterFactory.

In addition to exporting a type from a property we can also have function exports. i.e the [Export] attribute is tagged onto a method

 
  1. class FormatterFactory
  2. {
  3. [Export]
  4. public TextFormatter Formatter
  5. {
  6. get
  7. {
  8. returnnew TextFormatter();
  9. }
  10. }
  11. [Export]
  12. public TextFormatter GetTextFormatter()
  13. {
  14. returnnew TextFormatter();
  15. }
  16. [Export]
  17. publicvoid WriteFormattedOutput(string str, Stream stream)
  18. {
  19. System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
  20. var bytes = encoding.GetBytes(str);
  21. stream.Write(bytes,0,bytes.Length);
  22. }
  23. }

This enables some interesting scenarios for import, such as lazy evaulation.The function export can be imported into a type by specifying a delegate type that matches the function signature, The signatures of GetTextFormatter() and WriteFormattedOutput(string str, Stream stream) match the delegates Func<TextFormatter> and Action<string, Stream> respectively. We can use these delegates to import these exports into the ConsoleLogger.

 

 
  1. publicclass TextFormatter
  2. {
  3. publicstring Format(string str)
  4. {
  5. returnstring.Format("Formatted Message:{0}", str);
  6. }
  7. }
  8. [Export]
  9. publicclass ConsoleLogger
  10. {
  11. [Import]
  12. TextFormatter Formatter
  13. {
  14. get;
  15. set;
  16. }
  17. [Import]
  18. public Func<TextFormatter> FormatterFunc
  19. {
  20. get;
  21. privateset;
  22. }
  23. [Import]
  24. public Action<string, Stream> FormatterAction
  25. {
  26. get;
  27. privateset;
  28. }
  29. publicvoid Log(string str)
  30. {
  31. string formattedString = Formatter.Format(str);
  32. Console.WriteLine(formattedString);
  33. formattedString = FormatterFunc().Format(str);
  34. FormatterAction(str, Console.OpenStandardOutput());
  35. Console.WriteLine(formattedString);
  36. }
  37. }
  38. class Program
  39. {
  40. [Import]
  41. public ConsoleLogger Logger { get; set; }
  42. staticvoid Main(string[] args)
  43. {
  44. Program p = new Program();
  45. CompositionContainer container =
  46. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  47. container.ComposeParts(p);
  48. p.Logger.Log("This is a test");
  49. }
  50. }

Notice the following 2 lines in the Log method
formattedString = FormatterFunc().Format(str);
FormatterAction(str, Console.OpenStandardOutput());
FormatterFunc() invokes the GetTextFormatter() of the Export which gives us a TextFormatter on which we then invoke the Format method.
FormatterAction(str, Console.OpenStandardOutput()) invokes the WriteFormattedOutput method we exported passing in a string and the standard console stream.
This is immensely useful when you don't necessarily want MEF to materialize a value up front but rather provide a handle to a method which on invocation yields a value, but there are other ways to be lazy.

Using Interfaces instead of concrete classes

This far we've been using concrete classes for all exports and imports, i.e we've used ConsoleLogger and TextFormatter. This limits us to working only with these types. To promote loose coupling to any type, MEF also supports importing and exporting via interfaces. We can extract out the following interfaces from the ConsoleLogger and TextFormatter

 

 
  1. publicinterface ITextFormatter
  2. {
  3. string Name { get;}
  4. string Format(string str);
  5. }
  6. publicinterface ILogger
  7. {
  8. string Name { get;}
  9. ITextFormatter Formatter { get; set;}
  10. void Log(string str);
  11. }

and have both classes implement the respective interface

 

 
  1. [Export(typeof(ITextFormatter))]
  2. publicclass TextFormatter : ITextFormatter
  3. {
  4. publicstring Name
  5. {
  6. get
  7. {
  8. return"Text Formatter";
  9. }
  10. }
  11. publicstring Format(string str)
  12. {
  13. returnstring.Format("Formatted Message:{0}", str);
  14. }
  15. }
  16. [Export(typeof(ILogger))]
  17. publicclass ConsoleLogger : ILogger
  18. {
  19. [Import]
  20. public ITextFormatter Formatter
  21. {
  22. get;
  23. set;
  24. }
  25. publicvoid Log(string str)
  26. {
  27. string formattedString = Formatter.Format(str);
  28. Console.WriteLine(formattedString);
  29. }
  30. publicstring Name
  31. {
  32. get
  33. {
  34. return"Console Logger";
  35. }
  36. }
  37. }

Notice that the Export attributes now export the respective interface types and not the concrete types anymore and the Import attributes are applied on properties of interface types

 

 
  1. class Program
  2. {
  3. [Import]
  4. public ILogger Logger { get; set; }
  5. staticvoid Main(string[] args)
  6. {
  7. Program p = new Program();
  8. CompositionContainer container =
  9. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  10. container.ComposeParts(p);
  11. p.Logger.Log("This is a test");
  12. }
  13. }


The main program now just works since MEF resolves the import by instantiating the concrete type on which the Export attribute is defined, types it to the specified interface type and uses the interface type to satisfy imports.

Lazy Imports

It's great that MEF instantiates and injects types to satisfy imports but there are times when you just don't need an import to be satisfied until the point in time at which you need the imported type. The default behavior with the Import attribute is that when a part is composed, imports are satisified and MEF has already created an instance of the type and injected them into your type. Based on the dynamics of how your type is used, the satisifed import may never get used, this means that you paid for something that you did not need but paid for it in anticipation of it's use. It could very well be that the export might be a type which consumes expensive resources, or needs to make a cross domain/ cross process call as part of it's construction, or pull data from a database to initialize itself to a usable state, which means that we paid a "lot" for anticipation. To alleviate this, MEF leverages the Lazy<T> type which is new in .NET 4.0 to allow for lazy instantiaion of types. Using Lazy<T> for an import instead of T, causes MEF to delay the instantiation of the type T until the Value property of the Lazy<T> is accessed.

If we replace the Import in the main Program to use Lazy<ILogger> instead of ILogger, we effectively delay the creation of the ConsoleLogger until the Value property is accessed for the first time.

 
  1. [Export(typeof(ILogger))]
  2. publicclass ConsoleLogger : ILogger
  3. {
  4. [Import]
  5. public ITextFormatter Formatter
  6. {
  7. get;
  8. set;
  9. }
  10. publicvoid Log(string str)
  11. {
  12. string formattedString = Formatter.Format(str);
  13. Console.WriteLine(formattedString);
  14. }
  15. publicstring Name
  16. {
  17. get
  18. {
  19. return"Console Logger";
  20. }
  21. }
  22. }
  23. class Program
  24. {
  25. [Import]
  26. public Lazy<ILogger> Logger { get; set; }
  27. staticvoid Main(string[] args)
  28. {
  29. Program p = new Program();
  30. CompositionContainer container =
  31. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  32. container.ComposeParts(p);
  33. p.Logger.Value.Log("This is a test");
  34. }
  35. }


If you were to add a watch to the p.Logger.IsValueCreated, you would observe that it returns false.Once the Value property is accessed, IsValueCreated would return true.

Part Creation Policy

By default when MEF satisfies imports for a type(note the pluralization), it shares the instance that it creates from the export across all imports(i.e. it creates a singleton) and this is the default behvior. You can change this behavior on the importing side or the exporting side.

On the exporting side you can specify a PartCreationPolicy attribute on the type being exported indicating that a new instance should be created for satisying an import, by specifying a value of CreationPolicy.NonShared (the default that MEF uses is CreationPolicy.Shared)

 

 
  1. [Export(typeof(ILogger))]
  2. [PartCreationPolicy(CreationPolicy.NonShared)]
  3. publicclass ConsoleLogger : ILogger
  4. {
  5. [Import]
  6. public ITextFormatter Formatter
  7. {
  8. get;
  9. set;
  10. }
  11. publicvoid Log(string str)
  12. {
  13. string formattedString = Formatter.Format(str);
  14. Console.WriteLine(formattedString);
  15. }
  16. publicstring Name
  17. {
  18. get
  19. {
  20. return"Console Logger";
  21. }
  22. }
  23. }


Now, whenever MEF satisfies an import it will create a new instance of the ConsoleLogger instead of a singleton.
You can also specify a value of CreationPolicy.Any which effectively means that the importer decides the creation policy.
This is accomplished by using an overload of the Import attribute which accepts a RequiredCreationPolicy enum,which can have values of Shared,NonShared or Any. This is best illustrated with an example:

 

 
  1. [Export(typeof(ILogger))]
  2. [PartCreationPolicy(CreationPolicy.Any)]
  3. publicclass ConsoleLogger : ILogger
  4. {
  5. [Import]
  6. public ITextFormatter Formatter
  7. {
  8. get;
  9. set;
  10. }
  11. publicvoid Log(string str)
  12. {
  13. string formattedString = Formatter.Format(str);
  14. Console.WriteLine(formattedString);
  15. }
  16. publicstring Name
  17. {
  18. get
  19. {
  20. return"Console Logger";
  21. }
  22. }
  23. }
  24. class Program
  25. {
  26. [Import]
  27. public ILogger Logger1 { get; set; }
  28. [Import(RequiredCreationPolicy=CreationPolicy.Shared)]
  29. public ILogger Logger2 { get; set; }
  30. [Import(RequiredCreationPolicy=CreationPolicy.Shared)]
  31. public ILogger Logger3 { get; set; }
  32. [Import(RequiredCreationPolicy=CreationPolicy.NonShared)]
  33. public ILogger Logger4 { get; set; }
  34. staticvoid Main(string[] args)
  35. {
  36. Program p = new Program();
  37. CompositionContainer container =
  38. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  39. container.ComposeParts(p);
  40. }
  41. }

Since Logger2 and Logger3 have a CreationPolicy of CreationPolicy.Shared, they will share an instance of a ConsoleLogger that MEF creates, Logger1 will also share this instance since the default behavior is Shared if nothing is specified.
Logger4 on the other hand uses a CreationPolicy of CreationPolicy.NonShared, which will cause MEF to create a new instance of the ConsolLogger4 for satisfying the import for Logger4. This instance is not shared with any other import.

Default Values

There may be times where there is no available export to satisfy an import and it might be feasible to settle with a default value for an import (null for a reference type and the default value for a value type).
If a Formatter is not available, the logger can still perform logging like so:

 
  1. publicstruct LogInfo
  2. {
  3. privatestring _loggedBy ;
  4. publicstring LoggedBy
  5. {
  6. get { return _loggedBy; }
  7. }
  8. public LogInfo(string loggedBy)
  9. {
  10. _loggedBy = loggedBy;
  11. }
  12. }
  13. [Export(typeof(ILogger))]
  14. publicclass ConsoleLogger : ILogger
  15. {
  16. [Import(AllowDefault=true)]
  17. public ITextFormatter Formatter
  18. {
  19. get;
  20. set;
  21. }
  22. [Import(AllowDefault = true)]
  23. public LogInfo Info
  24. {
  25. get;
  26. set;
  27. }
  28. publicvoid Log(string str)
  29. {
  30. if (Formatter != null)
  31. {
  32. string formattedString = Formatter.Format(!string.IsNullOrEmpty(Info.LoggedBy)? str + " logged by" + Info.LoggedBy: str);
  33. Console.WriteLine(formattedString);
  34. }
  35. }
  36. publicstring Name
  37. {
  38. get
  39. {
  40. return"Console Logger";
  41. }
  42. }
  43. }
  44. class Program
  45. {
  46. [Import]
  47. public ILogger Logger { get; set; }
  48. staticvoid Main(string[] args)
  49. {
  50. Program p = new Program();
  51. CompositionContainer container =
  52. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  53. container.ComposeParts(p);
  54. }
  55. }

Note that the [Import] ttribute sets "AllowDefault" to true which causes the Formatter property to be set to null since there are no types which export IFormatter and hence the Formatter is set to null.
Also note that for kicks we have included a struct (LogInfo) and the ConsoleLogger imports this struct. Since this struct is not explicitly exported and "AllowDefault = true", MEF creates a default instance of the struct and sets the "Info" property to this default instance.

Named Exports

Since multiple types can export the same contract you can provide a name for each export so that you can uniquely identify an export on the importing side like so:

 
  1. [Export("Console Loger",typeof(ILogger))]
  2. publicclass ConsoleLogger : ILogger
  3. {
  4. [Import]
  5. public ITextFormatter Formatter
  6. {
  7. get;
  8. set;
  9. }
  10. publicvoid Log(string str)
  11. {
  12. string formattedString = Formatter.Format(str);
  13. Console.WriteLine(formattedString);
  14. }
  15. publicstring Name
  16. {
  17. get
  18. {
  19. return"Console Logger";
  20. }
  21. }
  22. }
  23. [Export("Debug Logger",typeof(ILogger))]
  24. publicclass DebugLogger : ILogger
  25. {
  26. [Import(AllowDefault = true)]
  27. public ITextFormatter Formatter
  28. {
  29. get;
  30. set;
  31. }
  32. publicvoid Log(string str)
  33. {
  34. string formattedString = Formatter.Format(str);
  35. Debug.WriteLine(formattedString);
  36. }
  37. publicstring Name
  38. {
  39. get
  40. {
  41. return"Debug Logger";
  42. }
  43. }
  44. }
  45. class Program
  46. {
  47. [Import("Debug Logger")]
  48. public ILogger Logger { get; set; }
  49. staticvoid Main(string[] args)
  50. {
  51. Program p = new Program();
  52. CompositionContainer container =
  53. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  54. container.ComposeParts(p);
  55. p.Logger.Log(DateTime.UtcNow.ToString());
  56. }
  57. }

The main drawback here is that we are now rely on strings for identifying a particular export. There are better ways to achieve the same end effect using attributes as we'll soon see.

Import Many

The main advantage of MEF is building wholes out of parts and building functionality by assembling parts. Multiple types can implement the same contract and export it. You are not limited to importing a single implementation of an export,multiple implementations of a contract can be imported using [ImportMany] You could have different types of loggers,a ConsoleLogger to log to the console, a DebugLogger to log to the debug output and so on. These loggers can all export the ILogger interface. A client application can import many such ILoggers which causes MEF to look for all matching available exports in the configured catalogs to satisfy the import. This is specified using an ImportMany attribute an IEnumerable<ILogger> like so:

 
  1. [Export(typeof(ITextFormatter))]
  2. publicclass TextFormatter : ITextFormatter
  3. {
  4. publicstring Name
  5. {
  6. get
  7. {
  8. return"Text Formatter";
  9. }
  10. }
  11. publicstring Format(string str)
  12. {
  13. returnstring.Format("Formatted Message:{0}", str);
  14. }
  15. }
  16. [Export(typeof(ILogger))]
  17. publicclass ConsoleLogger : ILogger
  18. {
  19. [Import]
  20. public ITextFormatter Formatter
  21. {
  22. get;
  23. set;
  24. }
  25. publicvoid Log(string str)
  26. {
  27. string formattedString = Formatter.Format(str);
  28. Console.WriteLine(formattedString);
  29. }
  30. publicstring Name
  31. {
  32. get
  33. {
  34. return"Console Logger";
  35. }
  36. }
  37. }
  38. [Export(typeof(ILogger))]
  39. publicclass DebugLogger : ILogger
  40. {
  41. [Import(AllowDefault = true)]
  42. public ITextFormatter Formatter
  43. {
  44. get;
  45. set;
  46. }
  47. publicvoid Log(string str)
  48. {
  49. string formattedString = Formatter.Format(str);
  50. Debug.WriteLine(formattedString);
  51. }
  52. publicstring Name
  53. {
  54. get
  55. {
  56. return"Debug Logger";
  57. }
  58. }
  59. }
  60. class Program
  61. {
  62. [ImportMany]
  63. public IEnumerable<ILogger> Loggers { get; set; }
  64. staticvoid Main(string[] args)
  65. {
  66. Program p = new Program();
  67. CompositionContainer container =
  68. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  69. container.ComposeParts(p);
  70. foreach (var logger in p.Loggers)
  71. {
  72. logger.Log(DateTime.UtcNow.ToString());
  73. }
  74. }
  75. }

Exporting Metadata

So now you've imported multiple exports,great!, you can use these exports as they are, but what if you needed to get a handle to a particular export you've imported. This is where metadata comes in. Each export can be annotated with export metadata which can be used to query the export.The metadata is made available on the import side as a Dictionary<string,object> which can be queried for the attribute name which is the dictionary key, and the value can be checked for the value of interest.
We can mark the ConsoleLogger as the default logger to use by specifying an ExportMetatdata attribute with key name "IsDefault" and value true.

 
  1. [Export(typeof(ITextFormatter))]
  2. publicclass TextFormatter : ITextFormatter
  3. {
  4. publicstring Name
  5. {
  6. get
  7. {
  8. return"Text Formatter";
  9. }
  10. }
  11. publicstring Format(string str)
  12. {
  13. returnstring.Format("Formatted Message:{0}", str);
  14. }
  15. }
  16. [Export(typeof(ILogger))]
  17. [ExportMetadata("IsDefault", true)]
  18. publicclass ConsoleLogger : ILogger
  19. {
  20. [Import]
  21. public ITextFormatter Formatter
  22. {
  23. get;
  24. set;
  25. }
  26. publicvoid Log(string str)
  27. {
  28. string formattedString = Formatter.Format(str);
  29. Console.WriteLine(formattedString);
  30. }
  31. publicstring Name
  32. {
  33. get
  34. {
  35. return"Console Logger";
  36. }
  37. }
  38. }
  39. [Export(typeof(ILogger))]
  40. [ExportMetadata("IsDefault", false)]
  41. publicclass DebugLogger : ILogger
  42. {
  43. [Import(AllowDefault = true)]
  44. public ITextFormatter Formatter
  45. {
  46. get;
  47. set;
  48. }
  49. publicvoid Log(string str)
  50. {
  51. string formattedString = Formatter.Format(str);
  52. Debug.WriteLine(formattedString);
  53. }
  54. publicstring Name
  55. {
  56. get
  57. {
  58. return"Debug Logger";
  59. }
  60. }
  61. }


The client can query for the default logger like so:

 
  1. class Program
  2. {
  3. [ImportMany]
  4. public IEnumerable<Lazy<ILogger,Dictionary<string,object>>> Loggers { get; set; }
  5. publicvoid LogUsingDefaultLogger(string message)
  6. {
  7. foreach (var logger in Loggers)
  8. {
  9. if (logger.Metadata.ContainsKey("IsDefault"))
  10. {
  11. bool isdefault = (bool)logger.Metadata["IsDefault"];
  12. if (isdefault)
  13. {
  14. logger.Value.Log(message);
  15. }
  16. }
  17. }
  18. }
  19. staticvoid Main(string[] args)
  20. {
  21. Program p = new Program();
  22. CompositionContainer container =
  23. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  24. container.ComposeParts(p);
  25. p.LogUsingDefaultLogger("test");
  26. }
  27. }


Note that in order to get to the metadata the Loggers property is of type IEnumerable<Lazy<ILogger,Dictionary<string,object>>>, Lazy<T> exposes a Metadata property which allows us to get to our metadata as shown in LogUsingDefaultLogger

Exporting Metadata the strongly typed way

Working with strings tends to be error prone.Wouldn't it be nicer to export metadata using strong types instead of strings. The good news is you can.You can write a custom attribute class derived from ExportAttribute and includes properties in the derived class which you can then use for finding exports matching your filter criteria

 
  1. [MetadataAttribute]
  2. [AttributeUsage(AttributeTargets.Class,AllowMultiple=false)]
  3. publicclass LogExportAttribute : ExportAttribute
  4. {
  5. public LogExportAttribute() : base(typeof(ILogger))
  6. {
  7. }
  8. publicbool IsDefault { get; set; }
  9. }


You can then decorate the loggers with this attribute and set one as the default

 
  1. [Export(typeof(ILogger))]
  2. [LogExport(IsDefault=true)]
  3. publicclass ConsoleLogger : ILogger
  4. {
  5. [Import]
  6. public ITextFormatter Formatter
  7. {
  8. get;
  9. set;
  10. }
  11. publicvoid Log(string str)
  12. {
  13. string formattedString = Formatter.Format(str);
  14. Console.WriteLine(formattedString);
  15. }
  16. publicstring Name
  17. {
  18. get
  19. {
  20. return"Console Logger";
  21. }
  22. }
  23. }
  24. [Export(typeof(ILogger))]
  25. [LogExport(IsDefault=false)]
  26. publicclass DebugLogger : ILogger
  27. {
  28. [Import(AllowDefault = true)]
  29. public ITextFormatter Formatter
  30. {
  31. get;
  32. set;
  33. }
  34. publicvoid Log(string str)
  35. {
  36. string formattedString = Formatter.Format(str);
  37. Debug.WriteLine(formattedString);
  38. }
  39. publicstring Name
  40. {
  41. get
  42. {
  43. return"Debug Logger";
  44. }
  45. }
  46. }


In order to query for the attribute and reach into it's properties we need to also define an interface with properties corresponding to that of the custom attribute class like so:

 
  1. publicinterface ILogExportMetdata
  2. {
  3. bool IsDefault { get; }
  4. }


We can then consume the metadata(strongly typed) like so:

 
  1. class Program
  2. {
  3. [ImportMany]
  4. public IEnumerable<Lazy<ILogger,ILogExportMetdata>> Loggers { get; set; }
  5. publicvoid LogUsingDefaultLogger(string message)
  6. {
  7. foreach (var logger in Loggers)
  8. {
  9. if (logger.Metadata.IsDefault)
  10. {
  11. logger.Value.Log(message);
  12. }
  13. }
  14. }
  15. staticvoid Main(string[] args)
  16. {
  17. Program p = new Program();
  18. CompositionContainer container =
  19. new CompositionContainer(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
  20. container.ComposeParts(p);
  21. p.LogUsingDefaultLogger("test");
  22. }
  23. }


Behind the scenes MEF performs some clever mapping of the attribute's properties to the interface's properties so that we can access the metadata in a strongly typed way.
This post is getting longer than I anticipated but we've covered the heart of what MEF is about.

 

 

  • Catalogs
  • Recomposition
  • Export Providers

Catalogs:
Catalogs provide one way for MEF to discover components that it can compose. In it's most basic form it contains a registration of types. A container by itself is just an empty repository. The catalog is the one that collects and returns ComposablePartDefinitions i.e (objects of types that you have registered with MEF) to the container which in turn uses this for satifying imports on a specified object. We've seen AssemblyCatalog previously as a means for finding and satisfying imports. The following catalogs are built into the framework: TypeCatalog, AssemblyCatalog, DirectoryCatalog and AggregateCatalog. Each of these catalogs are derived from ComposablePartCatalog which is the abstract base class for all catalogs.

TypeCatalog
A type catalog is an immutable catalog that lets you register one or more type names.When this catalog is provided to the container it will use the types specified in this catalog for resolving imports and exports. You can specify types directly using 'typeof' (less useful since you are coupled to concrete types) or read type information from a configuration file instead.

 
  1. [Export(typeof(ILogger))]
  2. publicclass ConsoleLogger : ILogger
  3. {
  4. [Import]
  5. public ITextFormatter Formatter
  6. {
  7. get;
  8. set;
  9. }
  10. publicvoid Log(string str)
  11. {
  12. string formattedString = Formatter.Format(str);
  13. Console.WriteLine(formattedString);
  14. }
  15. publicstring Name
  16. {
  17. get
  18. {
  19. return"Console Logger";
  20. }
  21. }
  22. }
  23. [Export(typeof(ITextFormatter))]
  24. publicclass TextFormatter : ITextFormatter
  25. {
  26. publicstring Name
  27. {
  28. get
  29. {
  30. return"Text Formatter";
  31. }
  32. }
  33. publicstring Format(string str)
  34. {
  35. returnstring.Format("Formatted Message:{0}", str);
  36. }
  37. }
  38. class Program
  39. {
  40. [Import]
  41. public ILogger Logger { get; set; }
  42. staticvoid Main(string[] args)
  43. {
  44. Program p = new Program();
  45. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(ConsoleLogger), typeof(TextFormatter));
  46. CompositionContainer container =
  47. new CompositionContainer(typeCatalog);
  48. container.ComposeParts(p);
  49. p.Logger.Log("Hello world");
  50. }
  51. }
To avoid coupling to specific type names (and by extension an assembly holding a concrete implementation) you could use 'Type.GetType(string typename)' instead where the typename could come from a config file. TypeCatalog gives you fine grained control over each type that you would like to register and make available to the container.

Assembly Catalog
If all the types that you would like to register are contained in an assembly you can use the AssemblyCatalog which scans the assembly and identifies the types that expose Import and Export attributes and registers them in one fell swoop into the catalog.This is useful when you have an application which is broken down into components by say feature areas or you have an on demand download for performance reasons or you have a pay as you go model for features available to your application as is typical in plugin based applications which is what MEF is all about anyways.
The AssemblyCatalog lets you register the types defined in such assemblies and make them available to the rest of the host application via the container.

 
  1. class Program
  2. {
  3. [Import]
  4. public ILogger Logger { get; set; }
  5. staticvoid Main(string[] args)
  6. {
  7. Program p = new Program();
  8. Assembly assembly = Assembly.LoadFile(Path.Combine(Directory.GetCurrentDirectory(), "MEF101.Implementations.dll"));
  9. //you could also load the assembly on the fly from over the wire by using Assemly.Load(byte[] bytes)
  10. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(assembly);
  11. CompositionContainer container = new CompositionContainer(assemblyCatalog);
  12. container.ComposeParts(p);
  13. p.Logger.Log("Hello World");
  14. }
  15. }

DirectoryCatalog
If all your types are contained in a specific directory (such as the current directory your exe is running from) you could use the DirectoryCatalog specifying a directory name and an optional search pattern to have MEF scan all assemblies in the specified directory and automatically register all types that it finds into the catalog like so:

 
  1. class Program
  2. {
  3. [Import]
  4. public MEF101.Interfaces.ILogger Logger { get; set; }
  5. staticvoid Main(string[] args)
  6. {
  7. Program p = new Program();
  8. ComposablePartCatalog dirCatalog = new DirectoryCatalog(Directory.GetCurrentDirectory());
  9. CompositionContainer container = new CompositionContainer(dirCatalog);
  10. container.ComposeParts(p);
  11. p.Logger.Log("Hello World");
  12. }
  13. }

AggregateCatalog
You can all combine all the above catalog types into an aggregate catalog type using the AggregateCatalog. As the name implies this takes in one or more ComposablePartCatalogs and exposes the the type registrations contained in all of them to the container as one whole. This is great when you have your types spread out over multiple locations. For example your applcation can start with a barebones set of features loaded from an assembly running in the current directory and you can asynchronously download updates/feature assemblies in the background. You can then compose your application using a combination of AssemblyCatalog and DirectoryCatalog
The following code shows a simplified example wherein ILogger implementations come from an AssemblyCatalog and a TypeCatalog .The TypeCatalog is added after the AggregateCatalog is constructed .

 
  1. [Export(typeof(MEF101.Interfaces.ILogger))]
  2. class DebugLogger : MEF101.Interfaces.ILogger
  3. {
  4. publicstring Name
  5. {
  6. get { return"Debug Logger"; }
  7. }
  8. [Import]
  9. public MEF101.Interfaces.ITextFormatter Formatter
  10. {
  11. get;
  12. set;
  13. }
  14. publicvoid Log(string str)
  15. {
  16. Debug.Write(str);
  17. }
  18. }
  19. class Program
  20. {
  21. [ImportMany]
  22. public IEnumerable<MEF101.Interfaces.ILogger> Loggers { get; set; }
  23. staticvoid Main(string[] args)
  24. {
  25. Program p = new Program();
  26. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(DebugLogger));
  27. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(Assembly.LoadFile(
  28. Path.Combine(Directory.GetCurrentDirectory(),
  29. "MEF101.Implementations.dll")));
  30. AggregateCatalog aggCatalog = new AggregateCatalog(assemblyCatalog);
  31. aggCatalog.Catalogs.Add(typeCatalog);
  32. CompositionContainer container = new CompositionContainer(aggCatalog);
  33. container.ComposeParts(p);
  34. foreach (var logger in p.Loggers)
  35. {
  36. logger.Log("Hello");
  37. }
  38. }
  39. }

Whereas all the other catalog types are immutable, the AggregateCatalog lets you add and subract ComposablePartCatalogs from it's internal "Catalog" collection facilitating recompositionof the container on the fly.

Recomposition
MEF provides a facility to allow a composable application to change it's behavior when composable parts can change on the fly via a mechanism called recomposition.
Let's re-write the example such that the call to "Catalogs.Add" is made after we compose the instance of Program

 
  1. [Export(typeof(MEF101.Interfaces.ILogger))]
  2. class DebugLogger : MEF101.Interfaces.ILogger
  3. {
  4. publicstring Name
  5. {
  6. get { return"Debug Logger"; }
  7. }
  8. [Import]
  9. public MEF101.Interfaces.ITextFormatter Formatter
  10. {
  11. get;
  12. set;
  13. }
  14. publicvoid Log(string str)
  15. {
  16. Debug.Write(str);
  17. }
  18. }
  19. class Program
  20. {
  21. [ImportMany]
  22. public IEnumerable<MEF101.Interfaces.ILogger> Loggers { get; set; }
  23. staticvoid Main(string[] args)
  24. {
  25. Program p = new Program();
  26. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(DebugLogger));
  27. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(Assembly.LoadFile(
  28. Path.Combine(Directory.GetCurrentDirectory(),
  29. "MEF101.Implementations.dll")));
  30. AggregateCatalog aggCatalog = new AggregateCatalog(assemblyCatalog);
  31. CompositionContainer container = new CompositionContainer(aggCatalog);
  32. container.ComposeParts(p);
  33. aggCatalog.Catalogs.Add(typeCatalog);
  34. foreach (var logger in p.Loggers)
  35. {
  36. logger.Log("Hello");
  37. }
  38. }
  39. }

This causes the following exception to be thrown:
Change in exports prevented by non-recomposable import 'MEF.LazyImports.Program.Loggers (ContractName="MEF101.Interfaces.ILogger")' on part 'MEF.LazyImports.Program'
This happens because the container changed after the instance of Program was already composed. The error indicates that the part being imported on the instance of Program i.e Loggers does not support recomposition and hence the change was prevented. We can easily enable this by changing the [Import] declaration on the Loggers property to
[ImportMany(AllowRecomposition=true)]. This allows for the program to dynamically obtain another instance of logger in it's Loggers collection and continue working normally
i.e. recompose itself
Similar to adding catalogs on the fly, we can also remove catalogs from the container and this will cause recomposition and update the Import on the instance of the Program automatically like so:
 
  1. [Export(typeof(MEF101.Interfaces.ILogger))]
  2. class DebugLogger : MEF101.Interfaces.ILogger
  3. {
  4. publicstring Name
  5. {
  6. get { return"Debug Logger"; }
  7. }
  8. [Import]
  9. public MEF101.Interfaces.ITextFormatter Formatter
  10. {
  11. get;
  12. set;
  13. }
  14. publicvoid Log(string str)
  15. {
  16. Debug.Write(str);
  17. }
  18. }
  19. class Program
  20. {
  21. [ImportMany]
  22. public IEnumerable<MEF101.Interfaces.ILogger> Loggers { get; set; }
  23. staticvoid Main(string[] args)
  24. {
  25. Program p = new Program();
  26. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(DebugLogger));
  27. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(Assembly.LoadFile(
  28. Path.Combine(Directory.GetCurrentDirectory(),
  29. "MEF101.Implementations.dll")));
  30. AggregateCatalog aggCatalog = new AggregateCatalog(assemblyCatalog);
  31. CompositionContainer container = new CompositionContainer(aggCatalog);
  32. container.ComposeParts(p);
  33. aggCatalog.Catalogs.Add(typeCatalog);
  34. foreach (var logger in p.Loggers)
  35. {
  36. logger.Log("Hello");
  37. }
  38. aggCatalog.Catalogs.Remove(typeCatalog);
  39. foreach (var logger in p.Loggers)
  40. {
  41. logger.Log("Hello");
  42. }
  43. }
  44. }

Notifications of Recomposition
MEF can notify you whenever a recomposition happens in case you need to perform some action such as cleanup/housekeeping. It does this via an interface called IPartImportSatisfiedNotification
You implement this interface in any of your parts in which you need that a recomposition(or more generally a composition) has occured.The interface has a single method called OnImportSatisfied
that you need to provide an implementation for.

 

 
  1. [Export(typeof(MEF101.Interfaces.ILogger))]
  2. class DebugLogger : MEF101.Interfaces.ILogger
  3. {
  4. publicstring Name
  5. {
  6. get { return"Debug Logger"; }
  7. }
  8. [Import]
  9. public MEF101.Interfaces.ITextFormatter Formatter
  10. {
  11. get;
  12. set;
  13. }
  14. publicvoid Log(string str)
  15. {
  16. Debug.Write(str);
  17. }
  18. }
  19. class Program : IPartImportsSatisfiedNotification
  20. {
  21. [ImportMany]
  22. public IEnumerable<MEF101.Interfaces.ILogger> Loggers { get; set; }
  23. publicvoid OnImportsSatisfied()
  24. {
  25. Console.WriteLine("Import satisfied");
  26. }
  27. staticvoid Main(string[] args)
  28. {
  29. Program p = new Program();
  30. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(DebugLogger));
  31. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(Assembly.LoadFile(
  32. Path.Combine(Directory.GetCurrentDirectory(),
  33. "MEF101.Implementations.dll")));
  34. AggregateCatalog aggCatalog = new AggregateCatalog(assemblyCatalog);
  35. CompositionContainer container = new CompositionContainer(aggCatalog);
  36. container.ComposeParts(p);
  37. aggCatalog.Catalogs.Add(typeCatalog);
  38. foreach (var logger in p.Loggers)
  39. {
  40. logger.Log("Hello");
  41. }
  42. aggCatalog.Catalogs.Remove(typeCatalog);
  43. foreach (var logger in p.Loggers)
  44. {
  45. logger.Log("Hello");
  46. }
  47. }
  48. }

In the above example, recomposition occurs thrice. Once for the initial composition: container.ComposeParts(p); and then once each for the calls to Catalogs.Add and Catalogs.Remove

Export Providers
Up until now we have used catalogs to initialize the CompositionContainer.We can also use one or more Export Providers to initialize the container. The difference is that by providing one or more export providers, MEF will search for exports matching an import in the order in which the exports providers are set on the CompositionContainer. This helps in situations where more than one catalog has part definitions that could satisfy an Import. Let's change the code above to illustrate this

 
  1. [Export(typeof(MEF101.Interfaces.ILogger))]
  2. class DebugLogger : MEF101.Interfaces.ILogger
  3. {
  4. publicstring Name
  5. {
  6. get { return"Debug Logger"; }
  7. }
  8. [Import]
  9. public MEF101.Interfaces.ITextFormatter Formatter
  10. {
  11. get;
  12. set;
  13. }
  14. publicvoid Log(string str)
  15. {
  16. Debug.Write(str);
  17. }
  18. }
  19. class Program
  20. {
  21. [Import(AllowRecomposition=true)]
  22. public MEF101.Interfaces.ILogger Logger { get; set; }
  23. staticvoid Main(string[] args)
  24. {
  25. Program p = new Program();
  26. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(DebugLogger));
  27. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(Assembly.LoadFile(
  28. Path.Combine(Directory.GetCurrentDirectory(),
  29. "MEF101.Implementations.dll")));
  30. AggregateCatalog aggCatalog = new AggregateCatalog(typeCatalog,assemblyCatalog);
  31. CompositionContainer container = new CompositionContainer(aggCatalog);
  32. container.ComposeParts(p);
  33. }
  34. }
The call to ComposeParts causes the following error.
More than one export was found that matches the constraint . The reason for this is because there are 2 parts (ConsoleLogger and DebugLogger) that are exported as ILogger and the Program has a single import for a logger instead of a collection of loggers. In this case MEF does not know which export to use for satisfying the import and hence the exception.
By using ExportProviders you can control the resolution order for satisying imports.

 
  1. [Export(typeof(MEF101.Interfaces.ILogger))]
  2. class DebugLogger : MEF101.Interfaces.ILogger
  3. {
  4. publicstring Name
  5. {
  6. get { return"Debug Logger"; }
  7. }
  8. [Import]
  9. public MEF101.Interfaces.ITextFormatter Formatter
  10. {
  11. get;
  12. set;
  13. }
  14. publicvoid Log(string str)
  15. {
  16. Debug.Write(str);
  17. }
  18. }
  19. class Program
  20. {
  21. [Import(AllowRecomposition=true)]
  22. public MEF101.Interfaces.ILogger Logger { get; set; }
  23. staticvoid Main(string[] args)
  24. {
  25. Program p = new Program();
  26. ComposablePartCatalog typeCatalog = new TypeCatalog(typeof(DebugLogger));
  27. ComposablePartCatalog assemblyCatalog = new AssemblyCatalog(Assembly.LoadFile(
  28. Path.Combine(Directory.GetCurrentDirectory(),
  29. "MEF101.Implementations.dll")));
  30. CatalogExportProvider assemblyProvider = new CatalogExportProvider(assemblyCatalog);
  31. CatalogExportProvider typeProvider = new CatalogExportProvider(typeCatalog);
  32. AggregateExportProvider aggProvider = new AggregateExportProvider(
  33. assemblyProvider, typeProvider);
  34. CompositionContainer container = new CompositionContainer(aggProvider);
  35. typeProvider.SourceProvider = container;
  36. assemblyProvider.SourceProvider = container;
  37. container.ComposeParts(p);
  38. p.Logger.Log("Hello");
  39. }
  40. }

Since the AggregateExportProvider contains the assemblyProvider first followed by typeProvider, the call to ComposeParts checks the assemblyProvider(and by extension the assemblyCatalog) first and notices that it can satisfy the import defined on Program, stops there and ends up using the ConsoleLogger. If no export was defined in the assemblyProvider then MEF would continue looking into the typeProvider for satisfying the import. Vice-versa, if the order was reversed and typeProvider was set before assemblyProvider then DebugLogger would have been used instead. You can also buid your own export providers by deriving from ExportProvider. This wraps up the coverage of MEF.
posted @ 2012-04-20 13:52  reagent  阅读(720)  评论(0编辑  收藏  举报