Part 1 dealt with fundamentals, imports and exports. Part 2 follows on from part 1 and explores additional features of the Managed Extensibility Framework (MEF). This time the focus is on metadata and creation policies.
Metadata
Exports can use metadata to expose additional information. To query this information, we use the class Lazy<>, use of which avoids causing the composable part to create an instance.
For our example application, we will go back to the example from part 1. We have an application (CarHost.exe) which uses imports to bind different cars (BMW.dll and Mercedes.dll). There is a contract (CarContract.dll) which contains the interface via which the host accesses the exports.
The metadata consist of three values. Firstly, a string containing the name (Name). Secondly, an enumeration indicating a colour (Color). Lastly, an integer containing the price (Price).
There are a number of options for how exports can make these metadata available to imports:
- non-type-safe
- type-safe via an interface
- type-safe via an interface and user-defined export attributes
- type-safe via an interface and enumerated user-defined export attributes
Option 1: non-type-safe
In this option, metadata are exposed using the ExportMetadata attribute. Each item of metadata is described using a name-value pair. The name is always a string type, whilst the value is an Object type. In some cases, it may be necessary to use the cast operator to explicitly convert a value to the required data type. In this case, the Price value needs to be converted to the uint data type.
We create two exports, each of which exposes the same metadata, but with differing values.
using System; using System.ComponentModel.Composition; using CarContract; namespace CarMercedes { [ExportMetadata("Name", "Mercedes")] [ExportMetadata("Color", CarColor.Blue)] [ExportMetadata("Price", (uint)48000)] [Export(typeof(ICarContract))] public class Mercedes : ICarContract { private Mercedes() { Console.WriteLine("Mercedes constructor."); } public string StartEngine(string name) { return String.Format("{0} starts the Mercedes.", name); } } } using System; using System.ComponentModel.Composition; using CarContract; namespace CarBMW { [ExportMetadata("Name", "BMW")] [ExportMetadata("Color", CarColor.Black)] [ExportMetadata("Price", (uint)55000)] [Export(typeof(ICarContract))] public class BMW : ICarContract { private BMW() { Console.WriteLine("BMW constructor."); } public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } }
The ICarContract interface exposes the method, so that it is then available to the import. It represents the ‘contract’ between the imports and the exports. The enumeration CarColor is also defined in the same namespace.
using System; namespace CarContract { public interface ICarContract { string StartEngine(string name); } public enum CarColor { Unkown, Black, Red, Blue, White } }
Metadata for the import can be accessed using the class Lazy<T, TMetadata>. This class exposes the Metadata property. Metadata is of type Dictionary<string, object>.
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using CarContract; namespace CarHost { class Program { [ImportMany(typeof(ICarContract))] private IEnumerable<Lazy<ICarContract, Dictionary<string, object>>> CarParts { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (Lazy<ICarContract, Dictionary<string, object>> car in CarParts) { if (car.Metadata.ContainsKey("Name")) Console.WriteLine(car.Metadata["Name"]); if (car.Metadata.ContainsKey("Color")) Console.WriteLine(car.Metadata["Color"]); if (car.Metadata.ContainsKey("Price")) Console.WriteLine(car.Metadata["Price"]); Console.WriteLine(""); } foreach (Lazy<ICarContract> car in CarParts) Console.WriteLine(car.Value.StartEngine("Sebastian")); container.Dispose(); } } }
If we want to access a specific item of metadata, we need to verify that the export has indeed defined the required item. It may well be that different imports expose different metadata.
On running the program, we can clearly see that accessing the metadata does not initialise the export parts. Only once we access the StartEngine() method do we create an instance, thereby calling the constructor.
Since the metadata is stored in a class of type Dictionary<string, object>, it can contain any number of items of metadata. This has advantages and disadvantages. The advantage is that all metadata are optional and the information they expose is entirely arbitrary – the value is of type Object. However, this also entails a loss of type safety. This is a major disadvantage. When accessing metadata, we always need to check that the metadata is actually present. Failure to do so can lead to some nasty runtime errors.
Sample 1 (Visual Studio 2010) on GitHub
Option 2: type-safe via an interface
Just as the available methods and properties of an export can be specified using an interface (ICarContract), it is also possible to define metadata using an interface. In this case, the individual values which will be available are specified using properties. You can only define properties which can be accessed using a get accessor. (If you try to define a set accessor, this will cause a runtime error.)
For our example, we will create three properties of the required type. We define an interface for the metadata as follows:
public interface ICarMetadata { string Name { get; } CarColor Color { get; } uint Price { get; } }
The interface for the metadata is used during verification between import and export. All exports must expose the defined metadata. If metadata are not present, this again results in a runtime error. If a property is optional, you can use the DefaultValue attribute.
[DefaultValue((uint)0)] uint Price { get; }
To avoid having to define all metadata in an export, all properties in this example will be decorated with the DefaultValue attribute.
using System; using System.ComponentModel; namespace CarContract { public interface ICarMetadata { [DefaultValue("NoName")] string Name { get; } [DefaultValue(CarColor.Unkown)] CarColor Color { get; } [DefaultValue((uint)0)] uint Price { get; } } }
The ICarContract interface and the exports are created in exactly the same way as in the first example.
To access the metadata, the interface for the metadata is used as the value for TMetadata in the Lazy<T, TMetadata> class. In this example, this is the ICarMetadata interface. Individual items of metadata are therefore available via the Metadata property.
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.Linq; using CarContract; namespace CarHost { class Program { [ImportMany(typeof(ICarContract))] IEnumerable<Lazy<ICarContract, ICarMetadata>> CarParts { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (Lazy<ICarContract, ICarMetadata> car in CarParts) { Console.WriteLine(car.Metadata.Name); Console.WriteLine(car.Metadata.Color); Console.WriteLine(car.Metadata.Price); Console.WriteLine(""); } // invokes the method only of black cars var blackCars = from lazyCarPart in CarParts let metadata = lazyCarPart.Metadata where metadata.Color == CarColor.Black select lazyCarPart.Value; foreach (ICarContract blackCar in blackCars) Console.WriteLine(blackCar.StartEngine("Sebastian")); Console.WriteLine("."); // invokes the method of all imports foreach (Lazy<ICarContract> car in CarParts) Console.WriteLine(car.Value.StartEngine("Sebastian")); container.Dispose(); } } }
Since the ICarMetadata interface specifies the name and type of the metadata, it can be accessed directly. This type-safety brings with it a small but useful advantage – it is now possible to access the CarParts property using LINQ. This means that it is possible to filter by metadata, so that only specific imports are used.
The first foreach loop outputs the metadata from all exports. The second uses LINQ to create a query which produces a list containing only those exports where the metadata has a specific value – in this case where Color has the value CarColor.Black. The StartEngine() method of these exports only is called. The final foreach loop calls this method for all exports.
Once again, we can clearly see that neither outputting all metadata nor the LINQ query initialises an export. A new instance is only created (and the constructor therefore called) on calling the StartEngine() method.
Sample 2 (Visual Studio 2010) on GitHub
In my opinion, interfaces should be used to work with metadata wherever possible. Sure, it may be a little more work, but this approach does avoid unwanted runtime errors.
Option 3: type-safe via an interface and user-defined export attributes
Defining metadata in the export has one further disadvantage. The name has to be supplied in the form of a string. With long names in particular, it’s easy for typos to creep in. Any typos will not be recognised by the compiler, producing errors which only become apparent at runtime. Of course things would be a lot easier if Visual Studio listed all valid metadata whilst typing and if the compiler noticed any typos. This happy state can be achieved by creating a separate attribute class for the metadata. To achieve this, all we need to do to our previous example is add a class.
using System; using System.ComponentModel.Composition; namespace CarContract { [MetadataAttribute] public class CarMetadataAttribute : Attribute { public string Name { get; set; } public CarColor Color { get; set; } public uint Price { get; set; } } }
This class needs to be decorated with the MetadataAttribute attribute and derived from the Attribute class. The individual values to be exported via the metadata are specified using properties. The type and name of the properties must match that specified in the interface for the metadata. We previously defined the ICarContract interface as follows:
using System; using System.ComponentModel; namespace CarContract { public interface ICarMetadata { [DefaultValue("NoName")] string Name { get; } [DefaultValue(CarColor.Unkown)] CarColor Color { get; } [DefaultValue((uint)0)] uint Price { get; } } }
We can not decorate an export with metadata by using this newly defined attribute.
[CarMetadata(Name="BMW", Color=CarColor.Black, Price=55000)] [Export(typeof(ICarContract))] public class BMW : ICarContract { // ... }
Now Visual Studio can give the developer a helping hand when entering metadata. All valid metadata are displayed during editing. In addition, the compiler is now in a position to verify that all of the entered metadata are valid.
Sample 3 (Visual Studio 2010) on GitHub
Option 4: type-safe via an interface and enumerated user-defined export attributes
Up to this point, it has not been possible to have multiple entries for a single item of metadata. However, there could be situations where we want an enumeration containing options which we wish to be able to combine together. We’re now going to extend our car example to allow us to additionally define the audio system with which the car is equipped. To do this, we first define an enum containing all of the possible options:
public enum AudioSystem { Without, Radio, CD, MP3 }
Now we add a property of type AudioSystem to the ICarMetadata interface.
using System; using System.ComponentModel; namespace CarContract { public interface ICarMetadata { [DefaultValue("NoName")] string Name { get; } [DefaultValue(CarColor.Unkown)] CarColor Color { get; } [DefaultValue((uint)0)] uint Price { get; } [DefaultValue(AudioSystem.Without)] AudioSystem[] Audio { get; } } }
Because a radio can also include a CD player, we need to be able to specify multiple options for specific items of metadata. In the export, the metadata is declared as follows:
using System; using System.ComponentModel.Composition; using CarContract; namespace CarBMW { [CarMetadata(Name="BMW", Color=CarColor.Black, Price=55000)] [CarMetadataAudio(AudioSystem.CD)] [CarMetadataAudio(AudioSystem.MP3)] [CarMetadataAudio(AudioSystem.Radio)] [Export(typeof(ICarContract))] public class BMW : ICarContract { private BMW() { Console.WriteLine("BMW constructor."); } public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } } using System; using System.ComponentModel.Composition; using CarContract; namespace CarMercedes { [CarMetadata(Name="Mercedes", Color=CarColor.Blue, Price=48000)] [CarMetadataAudio(AudioSystem.Radio)] [Export(typeof(ICarContract))] public class Mercedes : ICarContract { private Mercedes() { Console.WriteLine("Mercedes constructor."); } public string StartEngine(string name) { return String.Format("{0} starts the Mercedes.", name); } } }
Whilst the Mercedes has just a radio, the BMW also has a CD player and MP3 player.
To achieve this, we create an additional attribute class. This attribute class represents the metadata for the audio equipment (CarMetadataAudio).
using System; using System.ComponentModel.Composition; namespace CarContract { [MetadataAttribute] [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] public class CarMetadataAudioAttribute : Attribute { public CarMetadataAudioAttribute(AudioSystem audio) { this.Audio = audio; } public AudioSystem Audio { get; set; } } }
To allow us to specify multiple options for this attribute, this class has to be decorated with the AttributeUsage attribute and AllowMultiple needs to be set to true for this attribute. Here, the attribute class has been provided with a constructor, which takes the value directly as an argument.
Multiple metadata are output via an additional loop (see lines 28 and 29):
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.Linq; using CarContract; namespace CarHost { class Program { [ImportMany(typeof(ICarContract))] private IEnumerable<Lazy<ICarContract, ICarMetadata>> CarParts { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (Lazy<ICarContract, ICarMetadata> car in CarParts) { Console.WriteLine("Name: " + car.Metadata.Name); Console.WriteLine("Price: " + car.Metadata.Price.ToString()); Console.WriteLine("Color: " + car.Metadata.Color.ToString()); foreach (AudioSystem audio in car.Metadata.Audio) Console.WriteLine("Audio: " + audio); Console.WriteLine(""); } foreach (Lazy<ICarContract> car in CarParts) Console.WriteLine(car.Value.StartEngine("Sebastian")); container.Dispose(); } } }
Running the program yields the expected result:
Sample 4 (Visual Studio 2010) on GitHub
There is one further option, but this I will leave for a later post in which I will talk about inherited exports. It allows both the export and metadata to be decorated with an attribute simultaneously.
Creation Policies
In the previous examples, we used the Export and ImportMany attributes to bind multiple exports to a single import. But what does MEF do when multiple imports are available to a single export? This requires us to adapt the above example somewhat. The exports and the contract remain unchanged. In the host, instead of one list, we create two. Both lists take the same exports.
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using CarContract; namespace CarHost { class Program { [ImportMany(typeof(ICarContract))] private IEnumerable<Lazy<ICarContract>> CarPartsA { get; set; } [ImportMany(typeof(ICarContract))] private IEnumerable<Lazy<ICarContract>> CarPartsB { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (Lazy<ICarContract> car in CarPartsA) Console.WriteLine(car.Value.StartEngine("Sebastian")); Console.WriteLine(""); foreach (Lazy<ICarContract> car in CarPartsB) Console.WriteLine(car.Value.StartEngine("Michael")); container.Dispose(); } } }
This change means that two lists (imports) are assigned to each export. The program output, however, implies that each export is instantiated only once.
Sample 5 (Visual Studio 2010) on GitHub
If Managed Extensibility Framework finds a matching export for an import, it creates an instance of the export. This instance is shared with all other matching imports. MEF treats each export as a singleton.
We can modify this default behaviour both for exports and for imports by using the creation policy. Each creation policy can have the value Shared, NonShared or Any. The default setting is Any. An export for which the policy is defined as Shared or NonShared is only deemed to match an import if the creation policy of the import matches that of the export or is Any. To be considered matching, imports and exports must have compatible creation policies. If both imports and exports are defined as Any (or are undefined), both parts will be specified as Shared.
The creation policy for an export is defined using the PartCreationPolicy attribute.
[Export(typeof(ICarContract))] [PartCreationPolicy(CreationPolicy.NonShared)]
In the case of the Import or ImportAny attribute, the creation policy is defined by using the RequiredCreationPolicy property.
[ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.NonShared)] private IEnumerable<Lazy<ICarContract>> CarPartsA { get; set; }
The following output illustrates the case where the creation policy is set to NonShared. There are now two instances of each export.
Sample 6 (Visual Studio 2010) on GitHub
It is also possible to combine creation policies. For the import, I have decorated one list with NonShared and two further lists with Shared.
[ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.NonShared)] private IEnumerable<Lazy<ICarContract>> CarPartsA { get; set; } [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.Shared)] private IEnumerable<Lazy<ICarContract>> CarPartsB { get; set; } [ImportMany(typeof(ICarContract), RequiredCreationPolicy = CreationPolicy.Shared)] private IEnumerable<Lazy<ICarContract>> CarPartsC { get; set; }
The output shows how MEF creates the instances and assigns them to the individual imports:
The first list has its own independent instances of the exports. Lists two and three share the same instances.
Outlook
It is very encouraging that a framework of this kind has been standardised. Some Microsoft teams are already successfully using MEF, the best known example is Visual Studio. Let’s hope that more products will follow suit, and that this ensures that MEF continues to undergo further development.
Part 3 deals with the life cycle of composable parts.
Hi, Nice article and very well explained. But how i can modify the EXPORTMETADATA, the Tmetada part, when i want it “localized”. And an example will be apreciated. Example something like EXPORTMETADATA.
Thanks in advance
Nice work!, but can this be used to localize ExportMetada(string, object) where object is a string you want to add to some menu item??, something like ExportMetadata(“MenuText”,LocaizedText)???
So based off your above example if I have
private IEnumerable<Lazy> CarParts { get; set; }
Can I type cast the metadata object to a calss “CarMetadataAttribute” which implements ICarMetada?