It has become a necessary standard procedure to distribute applications among several components. Every developer who has designed such an infrastructure for dynamic extension knows that the implementation can be very sophisticated. Managed Extensibility Framework (MEF) promises relief. A short introduction should illustrate it.
Brief Review
Since .NET Framework 3.5 exists, there has been Managed AddIn Framework (MAF) which facilitates the work of developers. MAF can be found in the namespace System.AddIn. Main application and extensions are distinctly separated in MAF. Both of them use separate application domains. However, my first experiences with MAF were somewhat disappointing. The learning curve for MAF is very flat. The issue of MAF has also subsided. If you take a look at the Microsoft team blog, you will see that the last records are dated from 2008. Tools for Visual Studio which should simplify implementation do not appear to be fully mature or to be developed further.
Managed Extensibility Framework
As Managed Extensibility Framework (MEF) of .NET Framework 4 was announced, I put my activities by MAF aside for the moment. For the purpose of explaining, a small console application should be created step by step. It can dynamically be extended with components. Each component is a single DLL, where certain functions are stored.
For example, it might be certain calculations in a real application. But precisely these calculations can turn out differently from customer to customer. It is appropriate to perform calculation outside the main application, so that the whole program would not be compiled for each calculation variant. The basic program remains the same for all customers, only the components with calculations are individually adjusted. Report functions are also conceivable. They are provided for the main program separately. Thus, specific reports can be created without adapting the whole program.
A simple example with MEF
Our example should search all available DLLs in a current directory for a certain interface. If a DLL contains the demanded interface, it should be loaded and connected to the main program. Subsequently, the expected method of each component is invoked. The method has only one string as a parameter. This string is extended and transferred as a return parameter to the main application. Concretely, the example refers to car brands. Each component represents a car brand. The main application, or the host, loads all found components and calls the method through the common interface.
At first the common interface is declared, which implements all components:
using System; namespace CarHost { public interface ICarContract { string StartEngine(string name); } }
The interface is saved in a separate project, so that it is available for all other components as a single DLL. Both host and addins see only the contract (and have only one reference to it).
The next step is the first component, which should be dynamically added by the main application. The class Mercedes implements the interface ICarContract. In order the interface can be used, a reference to the DLL with the interface should be added to the project. Furthermore, the class with the attribute Export is decoded. A reference to System.ComponentModel.Composition should be added to the project, so that this attribute can be used. The attribute has several constructions. One of them expects a type as a parameter. The type denotes a contract. As a consequence, the class to be exported is assigned to a unique contract.
using System; using System.ComponentModel.Composition; namespace CarMercedes { [Export(typeof(CarHost.ICarContract))] public class Mercedes : CarHost.ICarContract { public string StartEngine(string name) { return String.Format("{0} starts the Mercedes.", name); } } }
Similarly, the class BMW implements ICarContract and contains also the Export attribute.
using System; using System.ComponentModel.Composition; namespace CarBMW { [Export(typeof(CarHost.ICarContract))] public class BMW : CarHost.ICarContract { public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } }
Let us switch to the main application, or the host. The task of the host is to search for those components in the current project directory which implement the interface ICarContract. These components should be loaded and made accessible to the host.
As a start, a reference to System.ComponentModel.Composition and ICarContract should be added to the project. In the array cars, all loaded components should be listed. For this purpose, the array with the attribute ImportMany will be decoded. The common contract will be again passed as a parameter.
using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace CarHost { class Program { [ImportMany(typeof(ICarContract))] private ICarContract[] cars = null; static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (ICarContract contract in cars) Console.WriteLine(contract.StartEngine("Sebastian")); } } }
The loading and the binding occurs in the method Run(). The classes ComposablePartCatalog and CompositionContainer are of central significance for MEF. The Catalog controls the loading of the components, whereas the CompositionContainer creates instances and links them to the corresponding variables. ComposablePartCatalog has several derivations. One of them is DirectoryCatalog. This class searches directory and loads all the components which are decoded with Import/ImportMany or Export. These elements are denoted as Composable Part. The Catalog is assigned to the container through the constructor. The instancing and binding processes are started with the method ComposeParts(). Sunsequently, the references to the found components are contained in the array. MEF ensures that only those components are loaded and linked which satisfy the contract.
When executing, the intended result is delivered. It is also clearly evident, which components our test project consists of. Our host CarHost.exe represents the central point. Moreover, we have the file CarContract.dll with the common contract, i.e. our interface. BMW.dll and Mercedes.dll are the components, which loaded and linked dynamically at runtime.
Sample 1 (Visual Studio 2013) on GitHub
This simple example clearly shows that with a help of three lines of code and two attributes, a program can be implemented which makes it possible to reload components dynamically. An own ‘manual’ implementation would certainly have become much more extensive. It can also be easily observed, that no new operator is needed to create the respective instances of the components. There is a loose coupling between the both program parts. The OO pattern Open/Closed and Design by Contract are thus also accomplished.
Export/Import of Methods
A Composable Part does not necessarily have to be a class. Methods, properties and fields are also possible. The following example binds methods to one another. For this purpose, the method is decorated with the attribute Export. The contract was defined with a string in this connection. Since the method will be later called via delegate, the interface ICarContract can be omitted.
using System; using System.ComponentModel.Composition; namespace CarBMW { public class BMW { [Export("CarContract")] public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } }
An array of a delegate Func is created in a main program. The delegate is defined in the namespace System. The corresponding method can now be called via delegate from the uploaded components.
using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace CarHost { class Program { [ImportMany("CarContract")] private Func[] startCars = null; static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); container.ComposeParts(this); foreach (Func start in startCars) Console.WriteLine(start("Sebastian")); } } }
Everything else corresponds to the first example.
Sample 2 (Visual Studio 2013) on GitHub
Import types
explicit contract name
In the following example, the contract name is declared directly for the attributes Import and Export. It makes sense to declare contract name directly under certain circumstances. This can be the case when one class exports several values which use the same type.
public class ClassA { [Import("MajorRevision")] public int MajorRevision; } public class ClassB { [Export("MajorRevision")] public int MajorRevision = 2; [Export("MinorRevision")] public int MinorRevision = 9; }
Even if the contract name is claimed directly, the contract type should exactly correspond between import and export. If the variable MajorRevision in ClassB were of type byte, it would have been impossible to bind import and export.
dynamic Imports
The binding can be structured more flexible with the help of dynamic import. In this case, the exact data type does not have to be specified in the course of import. Instead, the keyword dynamic is specified.
[ImportMany("MyCarContract")] private dynamic[] cars = null;
In such a case, you should always work with an explicit contract name. This contract name has also to be specified for the export.
[Export("MyCarContract")] public class BMW : CarHost.ICarContract
Needless to say, the importing class should be implemented in such a way that no run-time errors occur. The following example can, admittedly, be translated, but it throws an exception when executed.
foreach (var car in cars) Console.WriteLine(car.StopEngine("Sebastian"));
The method StopEngine() is not available in the imported class. A run-time error occurs. Accordingly, the usage of the keyword dynamic is not without some risks. Many errors distinguish themselves only during runtime.
delayed Imports
Since .NET Framework 4 exists, the class Lazy is supplied. This class can be used if an object should be initialized only at the first access.
[ImportMany(typeof(ICarContract))] private Lazy[] cars = null;
If the object has to be accessed, the property Value of the class Lazy should be used. The foreach loop will be as follows:
foreach (Lazy car in cars) Console.WriteLine(car.Value.StartEngine("Sebastian"));
You can understand well the workflow, when a standard constructor is added to the classes BMW and Mercedes and the foreach loop is removed from the main application. It will then be seen that the constructors are not called. Only when the foreach loop is integrated, the constructors are called.
Parameter Passing per Constructor
MEF always uses the standard constructor for instantiation of components. If another constructor has to be used, it needs to be decorated with the ImportingConstructor attribute. Only one constructor is allowed to own the ImportingConstructor attribute. A runtime error is thrown, if multiple constructors own the attribute. Also, a runtime error will be thrown, if there is no default constructor and no constructor has been decorated with the ImportingConstructor attribute. Noteworthy, constructors can be declared as private without any problem.
All parameters of the constructor are declared as Imports by default. Thus, there should be only one suitable Export in the host.
using System; using System.ComponentModel.Composition; namespace CarBMW { [Export(typeof(CarHost.ICarContract))] public class BMW : CarHost.ICarContract { [ImportingConstructor] private BMW(int parameter) { Console.WriteLine(String.Format("Parameter: {0}.", parameter)); } public string StartEngine(string name) { return String.Format("{0} starts the BMW.", name); } } } using System; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace CarHost { class Program { [Import(typeof(ICarContract))] private Lazy carPart { get; set; } [Export] private int Parameter { get; set; } static void Main(string[] args) { new Program().Run(); } void Run() { var catalog = new DirectoryCatalog("."); var container = new CompositionContainer(catalog); this.Parameter = 10; container.ComposeParts(this); Console.WriteLine(carPart.Value.StartEngine("Sebastian")); container.Dispose(); } } }
One can (and one also should) work with the contract names. It is necessary at the latest when several parameters of the same type are available. For this purpose, the Import attribute is specified explicitly before each parameter.
[ImportingConstructor] private BMW([Import("ConstructorParameter")]int parameter) { Console.WriteLine(String.Format("Parameter: {0}.", parameter)); }
The Export attribute in the host should certainly get the same contract name.
[Export("ConstructorParameter")] private int Parameter { get; set; }
Sample 3 (Visual Studio 2013)on GitHub
Optional Imports
It is always assumed that a corresponding export is available for each import. If it is not the case, an exception is thrown for the method ComposerParts(). The option AllowDefault can be chosen by the Import attribute to change this behaviour.
[Import(typeof(ICarContract), AllowDefault = true)] private ICarContract carPart = null;
If no corresponding export is available, an exception is not thrown by the calling of ComposeParts(), and the variable carPart remains unchanged equal to null.
This option is not available for the ImportMany attribute. It is as well not needed. If there is no corresponding export for ImportMany, ComposeParts() creates a blank list.
Prevent an Export
The contract name and the type should match, so that an export can be bound to an import. There are two more reasons which prevent a binding.
If the Export attribute is set for an abstract class, the export is not available.
[Export] public abstract class Mercedes { // ... }
Additionally, the export can be prevented with the PartNotDiscoverable attribute.
[PartNotDiscoverable] [Export] public class Mercedes { // ... }
Up to this point, only the most important fundamentals of MEF could be illustrated. The potential is by far not exhausted. That is why the meta data and construction guidelines will be described in Part 2.