Export (0) Print
Expand All

Attributed Programming Model Overview

In the Managed Extensibility Framework (MEF), a programming model is a particular method of defining the set of conceptual objects on which MEF operates. These conceptual objects include parts, imports, and exports. MEF uses these objects, but does not specify how they should be represented. Therefore, a wide variety of programming models are possible, including customized programming models.

The default programming model used in MEF is the attributed programming model. In the attributed programming model parts, imports, exports, and other objects are defined with attributes that decorate ordinary .NET Framework classes. This topic explains how to use the attributes provided by the attributed programming model to create a MEF application.

This topic contains the following sections.

An export is a value that a part provides to other parts in the container, and an import is a requirement that a part expresses to the container, to be filled from the available exports. In the attributed programming model, imports and exports are declared by decorating classes or members with the Import and Export attributes. The Export attribute can decorate a class, field, property, or method, while the Import attribute can decorate a field, property, or constructor parameter.

In order for an import to be matched with an export, the import and export must have the same contract. The contract consists of a string, called the contract name, and the type of the exported or imported object, called the contract type. Only if both the contract name and contract type match is an export considered to fulfill a particular import.

Either or both of the contract parameters can be implicit or explicit. The following code shows a class that declares a basic import.

public class MyClass
{
    [Import]
    public IMyAddin MyAddin { get; set; }
}

In this import, the Import attribute has neither a contract type nor a contract name parameter attached. Therefore, both will be inferred from the decorated property. In this case, the contract type will be IMyAddin, and the contract name will be a unique string created from the contract type. (In other words, the contract name will match only exports whose names are also inferred from the type IMyAddin.)

The following shows an export that matches the previous import.

[Export(typeof(IMyAddin))]
public class MyLogger : IMyAddin { }

In this export, the contract type is IMyAddin because it is specified as a parameter of the Export attribute. The exported type must be either the same as the contract type, derive from the contract type, or implement the contract type if it is an interface. In this export, the actual type MyLogger implements the interface IMyAddin. The contract name is inferred from the contract type, which means that this export will match the previous import.

NoteNote

Exports and imports should usually be declared on public classes or members. Other declarations are supported, but exporting or importing a private, protected, or internal member breaks the isolation model for the part and is therefore not recommended.

The contract type must match exactly for the export and import to be considered a match. Consider the following export.

[Export] //WILL NOT match the previous import!
public class MyLogger : IMyAddin { }

In this export, the contract type is MyLogger instead of IMyAddin. Even though MyLogger implements IMyAddin, and therefore could be cast to an IMyAddin object, this export will not match the previous import because the contract types are not the same.

In general, it is not necessary to specify the contract name, and most contracts should be defined in terms of the contract type and metadata. However, under certain circumstances, it is important to specify the contract name directly. The most common case is when a class exports several values that share a common type, such as primitives. The contract name can be specified as the first parameter of the Import or Export attribute. The following code shows an import and an export with a specified contract name of MajorRevision.

public class MyClass
{
    [Import("MajorRevision")]
    public int MajorRevision { get; set; }
}

public class MyExportClass
{ 
    [Export("MajorRevision")] //This one will match.
    public int MajorRevision = 4;

    [Export("MinorRevision")]
    public int MinorRevision = 16;
}

If the contract type is not specified, it will still be inferred from the type of the import or export. However, even if the contract name is specified explicitly, the contract type must also match exactly for the import and export to be considered a match. For example, if the MajorRevision field was a string, the inferred contract types would not match and the export would not match the import, despite having the same contract name.

The Export attribute can also decorate a method, in the same way as a class, property, or function. Method exports must specify a contract type or contract name, as the type cannot be inferred. The specified type can be either a custom delegate or a generic type, such as Func. The following class exports a method named DoSomething.

public class MyAddin
{
    //Explicitly specifying a generic type.
    [Export(typeof(Func<int, string>)]
    public string DoSomething(int TheParam);
}

In this class, the DoSomething method takes a single int parameter and returns a string. To match this export, the importing part must declare an appropriate member. The following class imports the DoSomething method.

public class MyClass
{
    [Import] //Contract name must match!
    public Func<int, string> DoSomething { get; set; }
}

For more information about how to use of the Func<T, T> object, see Func<T, TResult>.

MEF support several import types, including dynamic, lazy, prerequisite, and optional.

In some cases, the importing class may want to match exports of any type that have a particular contract name. In this scenario, the class can declare a dynamic import. The following import matches any export with contract name "TheString".

public class MyClass
{
    [Import("TheString")]
    public dynamic MyAddin { get; set; }
}

When the contract type is inferred from the dynamic keyword, it will match any contract type. In this case, an import should always specify a contract name to match on. (If no contract name is specified, the import will be considered to match no exports.) Both of the following exports would match the previous import.

[Export("TheString", typeof(IMyAddin))]
public class MyLogger : IMyAddin { }

[Export("TheString")]
public class MyToolbar { }

Obviously, the importing class must be prepared to deal with an object of arbitrary type.

In some cases, the importing class may require an indirect reference to the imported object, so that the object is not instantiated immediately. In this scenario, the class can declare a lazy import by using a contract type of Lazy<T>. The following importing property declares a lazy import.

public class MyClass
{
    [Import]
    public Lazy<IMyAddin> MyAddin { get; set; }
}

From the point of view of the composition engine, a contract type of Lazy<T> is considered identical to contract type of T. Therefore, the previous import would match the following export.

[Export(typeof(IMyAddin))]
public class MyLogger : IMyAddin { }

The contract name and contract type can be specified in the Import attribute for a lazy import, as described earlier in the "Basic Imports and Exports" section.

Exported MEF parts are typically created by the composition engine, in response to a direct request or the need to fill a matched import. By default, when creating a part, the composition engine uses the parameter-less constructor. To make the engine use a different constructor, you can mark it with the ImportingConstructor attribute.

Each part may have only one constructor for use by the composition engine. Providing no default constructor and no ImportingConstructor attribute, or providing more than one ImportingConstructor attribute, will produce an error.

To fill the parameters of a constructor marked with the ImportingConstructor attribute, all of those parameters are automatically declared as imports. This is a convenient way to declare imports that are used during part initialization. The following class uses ImportingConstructor to declare an import.

public class MyClass 
{
    private IMyAddin _theAddin;

    //Default constructor will NOT be
    //used because the ImportingConstructor
    //attribute is present.
    public MyClass() { }

    //This constructor will be used.
    //An import with contract type IMyAddin is 
    //declared automatically.
    [ImportingConstructor] 
    public MyClass(IMyAddin MyAddin) 
    {
        _theAddin = MyAddin;
    }
}

By default, the ImportingConstructor attribute uses inferred contract types and contract names for all of the parameter imports. It is possible to override this by decorating the parameters with Import attributes, which can then define the contract type and contract name explicitly. The following code demonstrates a constructor that uses this syntax to import a derived class instead of a parent class.

    [ImportingConstructor] 
    public MyClass([Import(typeof(IMySubAddin))]IMyAddin MyAddin) 
    {
        _theAddin = MyAddin;
    }

In particular, you should be careful with collection parameters. For example, if you specify ImportingConstructor on a constructor with a parameter of type IEnumerable<int>, the import will match a single export of type IEnumerable<int>, instead of a set of exports of type int. To match a set of exports of type int, you have to decorate the parameter with the ImportMany attribute.

Parameters declared as imports by the ImportingConstructor attribute are also marked as prerequisite imports. MEF normally allows exports and imports to form a cycle. For example, a cycle is where object A imports object B, which in turn imports object A. Under ordinary circumstances, a cycle is not a problem, and the composition container constructs both objects normally.

When an imported value is required by the constructor of a part, that object cannot participate in a cycle. If object A requires that object B be constructed before it can be constructed itself, and object B imports object A, then the cycle will be unable to resolve and a composition error will occur. Imports declared on constructor parameters are therefore prerequisite imports, which must all be filled before any of the exports from the object that requires them can be used.

The Import attribute specifies a requirement for the part to function. If an import cannot be fulfilled, the composition of that part will fail and the part will not be available.

You can specify that an import is optional by using the AllowDefault property. In this case, the composition will succeed even if the import does not match any available exports, and the importing property will be set to the default for its property type (null for object properties, false for Booleans, or zero for numeric properties.) The following class uses an optional import.

public class MyClass
{
    [Import(AllowDefault = true)]
    public Plugin thePlugin { get; set; }

    //If no matching export is avaliable,
    //thePlugin will be set to null.
}

The Import attribute will only be successfully composed when it matches one and only one export. Other cases will produce a composition error. To import more than one export that matches the same contract, use the ImportMany attribute. Imports marked with this attribute are always optional. For example, composition will not fail if no matching exports are present. The following class imports any number of exports of type IMyAddin.

public class MyClass
{
    [ImportMany]
    public IEnumerable<IMyAddin> MyAddin { get; set; }
}

The imported array can be accessed by using ordinary IEnumerable<T> syntax and methods. It is also possible to use an ordinary array (IMyAddin[]) instead.

This pattern can be very important when you use it in combination with the Lazy<T> syntax. For example, by using ImportMany, IEnumerable<T>, and Lazy<T>, you can import indirect references to any number of objects and only instantiate the ones that become necessary. The following class shows this pattern.

public class MyClass
{
    [ImportMany]
    public IEnumerable<Lazy<IMyAddin>> MyAddin { get; set; }
}

In some cases, you may want to prevent a part from being discovered as part of a catalog. For example, the part may be a base class intended to be inherited from, but not used. There are two ways to accomplish this. First, you can use the abstract keyword on the part class. Abstract classes never provide exports, although they can provide inherited exports to classes that derive from them.

If the class cannot be made abstract, you can decorate it with the PartNotDiscoverable attribute. A part decorated with this attribute will not be included in any catalogs. The following example demonstrates these patterns. DataOne will be discovered by the catalog. Since DataTwo is abstract, it will not be discovered. Since DataThree used the PartNotDiscoverable attribute, it will not be discovered.

[Export]
public class DataOne
{
    //This part will be discovered
    //as normal by the catalog.
}

[Export]
public abstract class DataTwo
{
    //This part will not be discovered
    //by the catalog.
}

[PartNotDiscoverable]
[Export]
public class DataThree
{
    //This part will also not be discovered
    //by the catalog.
}

Exports can provide additional information about themselves known as metadata. Metadata can be used to convey properties of the exported object to the importing part. The importing part can use this data to decide which exports to use, or to gather information about an export without having to construct it. For this reason, an import must be lazy to use metadata.

To use metadata, you typically declare an interface known as a metadata view, which declares what metadata will be available. The metadata view interface must have only properties, and those properties must have get accessors. The following interface is an example metadata view.

public interface IPluginMetadata
{
    string Name { get; }

    [DefaultValue(1)]  
    int Version { get; }
}

It is also possible to use a generic collection, IDictionary<string, object>, as a metadata view, but this forfeits the benefits of type checking and should be avoided.

Ordinarily, all of the properties named in the metadata view are required, and any exports that do not provide them will not be considered a match. The DefaultValue attribute specifies that a property is optional. If the property is not included, it will be assigned the default value specified as a parameter of DefaultValue. The following are two different classes decorated with metadata. Both of these classes would match the previous metadata view.

[Export(typeof(IPlugin)),
    ExportMetadata("Name", "Logger"),
    ExportMetadata("Version", 4)]
public class Logger : IPlugin
{
}

[Export(typeof(IPlugin)),
    ExportMetadata("Name", "Disk Writer")] 
    //Version is not required because of the DefaultValue
public class DWriter : IPlugin
{
}

Metadata is expressed after the Export attribute by using the ExportMetadata attribute. Each piece of metadata is composed of a name/value pair. The name portion of the metadata must match the name of the appropriate property in the metadata view, and the value will be assigned to that property.

It is the importer that specifies what metadata view, if any, will be in use. An import with metadata is declared as a lazy import, with the metadata interface as the second type parameter to Lazy<T,T>. The following class imports the previous part with metadata.

public class Addin
{
    [Import]
    public Lazy<IPlugin, IPluginMetadata> plugin;
}

In many cases, you will want to combine metadata with the ImportMany attribute, in order to parse through the available imports and choose and instantiate only one, or filter a collection to match a certain condition. The following class instantiates only IPlugin objects that have the Name value "Logger".

public class User
{
    [ImportMany]
    public IEnumerable<Lazy<IPlugin, IPluginMetadata>> plugins;

    public IPlugin InstantiateLogger ()
    {
        IPlugin logger = null;

        foreach (Lazy<IPlugin, IPluginMetadata> plugin in plugins)
        {
            if (plugin.Metadata.Name = "Logger") logger = plugin.Value;
        }
        return logger;
    }
}

If a class inherits from a part, that class may also become a part. Imports are always inherited by subclasses. Therefore, a subclass of a part will always be a part, with the same imports as its parent class.

Exports declared by using the Export attribute are not inherited by subclasses. However, a part can export itself by using the InheritedExport attribute. Subclasses of the part will inherit and provide the same export, including contract name and contract type. Unlike an Export attribute, InheritedExport can be applied only at the class level, and not at the member level. Therefore, member-level exports can never be inherited.

The following four classes demonstrate the principles of import and export inheritance. NumTwo inherits from NumOne, so NumTwo will import IMyData. Ordinary exports are not inherited, so NumTwo will not export anything. NumFour inherits from NumThree. Because NumThree used InheritedExport, NumFour has one export with contract type NumThree. Member-level exports are never inherited, so IMyData is not exported.

[Export]
public class NumOne
{
    [Import]
    public IMyData MyData { get; set; }
}

public class NumTwo : NumOne
{
    //Imports are always inherited, so NumTwo will 
    //import IMyData.

    //Ordinary exports are not inherited, so 
    //NumTwo will NOT export anything. As a result it 
    //will not be discovered by the catalog!
}

[InheritedExport]
public class NumThree
{
    [Export]
    Public IMyData MyData { get; set; }

    //This part provides two exports, one of
    //contract type NumThree, and one of
    //contract type IMyData.
}

public class NumFour : NumThree
{
    //Because NumThree used InheritedExport,
    //this part has one export with contract
    //type NumThree.

    //Member-level exports are never inherited,
    //so IMyData is not exported.
}

If there is metadata associated with an InheritedExport attribute, that metadata will also be inherited. (For more information, see the earlier "Metadata and Metadata Views" section.) Inherited metadata cannot be modified by the subclass. However, by re-declaring the InheritedExport attribute with the same contract name and contract type, but with new metadata, the subclass can replace the inherited metadata with new metadata. The following class demonstrates this principle. The MegaLogger part inherits from Logger and includes the InheritedExport attribute. Since MegaLogger re-declares new metadata named Status, it does not inherit the Name and Version metadata from Logger.

[InheritedExport(typeof(IPlugin)),
    ExportMetadata("Name", "Logger"),
    ExportMetadata("Version", 4)]
public class Logger : IPlugin
{
    //Exports with contract type IPlugin and 
    //metadata "Name" and "Version".
}

public class SuperLogger : Logger
{
    //Exports with contract type IPlugin and 
    //metadata "Name" and "Version", exactly the same
    //as the Logger class.
}

[InheritedExport(typeof(IPlugin)),
    ExportMetadata("Status", "Green")]
public class MegaLogger : Logger        {
    //Exports with contract type IPlugin and 
    //metadata "Status" only. Re-declaring 
    //the attribute replaces all metadata.
}

When re-declaring the InheritedExport attribute to override metadata, make sure that the contract types are the same. (In the previous example, IPlugin is the contract type.) If they differ, instead of overriding, the second attribute will create a second, independent export from the part. Generally, this means that you will have to explicitly specify the contract type when you override an InheritedExport attribute, as shown in the previous example.

Since interfaces cannot be instantiated directly, they generally cannot be decorated with Export or Import attributes. However, an interface can be decorated with an InheritedExport attribute at the interface level, and that export along with any associated metadata will be inherited by any implementing classes. The interface itself will not be available as a part, however.

The basic export attributes, Export and InheritedExport, can be extended to include metadata as attribute properties. This technique is useful for applying similar metadata to many parts, or creating an inheritance tree of metadata attributes.

A custom attribute can specify the contract type, the contract name, or any other metadata. In order to define a custom attribute, a class inheriting from ExportAttribute (or InheritedExportAttribute) must be decorated with the MetadataAttribute attribute. The following class defines a custom attribute.

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class MyAttribute : ExportAttribute
{
    public MyAttribute(string myMetadata) 
        : base(typeof(IMyAddin))
    {
        MyMetadata = myMetadata;
    }

    public string MyMetadata { get; private set; }
}

This class defines a custom attribute named MyAttribute with contract type IMyData and some metadata named MyMetadata. All properties in a class marked with the MetadataAttribute attribute are considered to be metadata defined in the custom attribute. The following two declarations are equivalent.

[Export(typeof(IMyAddin)), 
    ExportMetadata("MyMetadata", "theData")]
public MyAddin myAddin { get; set; }
[MyAttribute("theData")]
public MyAddin myAddin { get; set; }

In the first declaration, the contract type and metadata are explicitly defined. In the second declaration, the contract type and metadata are implicit in the customized attribute. Particularly in cases where a large amount of identical metadata must be applied to many parts (for example, author or copyright information), using a custom attribute can save a lot of time and duplication. Further, inheritance trees of custom attributes can be created to allow for variations.

To create optional metadata in a custom attribute, you can use the DefaultValue attribute. When this attribute is applied to a property in a custom attribute class, it specifies that the decorated property is optional and does not have to be supplied by an exporter. If a value for the property is not supplied, it will be assigned the default value for its property type (usually null, false, or 0.)

When a part specifies an import and composition is performed, the composition container attempts to find a matching export. If it matches the import with an export successfully, the importing member is set to an instance of the exported object. Where this instance comes from is controlled by the exporting part's creation policy.

The two possible creation policies are shared and non-shared. A part with a creation policy of shared will be shared between every import in the container for a part with that contract. When the composition engine finds a match and has to set an importing property, it will instantiate a new copy of the part only if one does not already exist; otherwise, it will supply the existing copy. This means that many objects may have references to the same part. Such parts should not rely on internal state that might be changed from many places. This policy is appropriate for static parts, parts that provide services, and parts that consume a lot of memory or other resources.

A part with the creation policy of non-shared will be created every time a matching import for one of its exports is found. A new copy will therefore be instantiated for every import in the container that matches one of the part's exported contracts. The internal state of these copies will not be shared. This policy is appropriate for parts where each import requires its own internal state.

Both the import and the export can specify the creation policy of a part, from among the values Shared, NonShared, or Any. The default is Any for both imports and exports. An export that specifies Shared or NonShared will only match an import that specifies the same, or that specifies Any. Similarly, an import that specifies Shared or NonShared will only match an export that specifies the same, or that specifies Any. Imports and exports with incompatible creation policies are not considered a match, in the same way as an import and export whose contract name or contract type are not a match. If both import and export specify Any, or do not specify a creation policy and default to Any, the creation policy will default to shared.

The following example shows both imports and exports specifying creation policies. PartOne does not specify a creation policy, so the default is Any. PartTwo does not specify a creation policy, so the default is Any. Since both import and export default to Any, PartOne will be shared. PartThree specifies a Shared creation policy, so PartTwo and PartThree will share the same copy of PartOne. PartFour specifies a NonShared creation policy, so PartFour will be non-shared in PartFive. PartSix specifies a NonShared creation policy. PartFive and PartSix will each receive separate copies of PartFour. PartSeven specifies a Shared creation policy. Because there is no exported PartFour with a creation policy of Shared, the PartSeven import does not match anything and will not be filled.

[Export]
public class PartOne
{
    //The default creation policy for an export is Any.
}

public class PartTwo
{
    [Import]
    public PartOne partOne { get; set; }

    //The default creation policy for an import is Any.
    //If both policies are Any, the part will be shared.
}

public class PartThree
{
    [Import(RequiredCreationPolicy = CreationPolicy.Shared)]
    public PartOne partOne { get; set; }

    //The Shared creation policy is explicitly specified.
    //PartTwo and PartThree will receive references to the
    //SAME copy of PartOne.
}

[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class PartFour
{
    //The NonShared creation policy is explicitly specified.
}

public class PartFive
{
    [Import]
    public PartFour partFour { get; set; }

    //The default creation policy for an import is Any.
    //Since the export's creation policy was explictly
    //defined, the creation policy for this property will
    //be non-shared.
}

public class PartSix
{
    [Import(RequiredCreationPolicy = CreationPolicy.NonShared)]
    public PartFour partFour { get; set; }

    //Both import and export specify matching creation 
    //policies.  PartFive and PartSix will each receive 
    //SEPARATE copies of PartFour, each with its own
    //internal state.
}

public class PartSeven
{
    [Import(RequiredCreationPolicy = CreationPolicy.Shared)]
    public PartFour partFour { get; set; }

    //A creation policy mismatch.  Because there is no 
    //exported PartFour with a creation policy of Shared,
    //this import does not match anything and will not be
    //filled.
}

Because parts are hosted in the composition container, their life cycle can be more complex than ordinary objects. Parts can implement two important life cycle-related interfaces: IDisposable and IPartImportsSatisfiedNotification.

Parts that require work to be performed at shut down or that need to release resources should implement IDisposable, as usual for .NET Framework objects. However, since the container creates and maintains references to parts, only the container that owns a part should call the Dispose method on it. The container itself implements IDisposable, and as portion of its cleanup in Dispose it will call Dispose on all the parts that it owns. For this reason, you should always dispose the composition container when it and any parts it owns are no longer needed.

For long-lived composition containers, memory consumption by parts with a creation policy of non-shared can become a problem. These non-shared parts can be created multiple times and will not be disposed until the container itself is disposed. To deal with this, the container provides the ReleaseExport method. Calling this method on a non-shared export removes that export from the composition container and disposes it. Parts that are used only by the removed export, and so on down the tree, are also removed and disposed. In this way, resources can be reclaimed without disposing the composition container itself.

IPartImportsSatisfiedNotification contains one method named OnImportsSatisfied. This method is called by the composition container on any parts that implement the interface when composition has been completed and the part's imports are ready for use. Parts are created by the composition engine to fill the imports of other parts. Before the imports of a part have been set, you cannot perform any initialization that relies on or manipulates imported values in the part constructor unless those values have been specified as prerequisites by using the ImportingConstructor attribute. This is normally the preferred method, but in some cases, constructor injection may not be available. In those cases, initialization can be performed in OnImportsSatisfied, and the part should implement IPartImportsSatisfiedNotification.

Show:
© 2014 Microsoft