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 MyClass1
        <Import()>
        Public Property MyAddin As IMyAddin
    End Class

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(GetType(IMyAddin))>
    Public Class MyLogger
        Implements IMyAddin

    End Class

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
        Implements IMyAddin

    End Class

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 MyExportClass

        'This one will match
        <Export("MajorRevision")>
        Public ReadOnly Property MajorRevision As Integer
            Get
                Return 4
            End Get
        End Property

        <Export("MinorRevision")>
        Public ReadOnly Property MinorRevision As Integer
            Get
                Return 16
            End Get
        End Property
    End Class

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(GetType(Func(Of Integer, String)))>
        Public Function DoSomething(ByVal TheParam As Integer) As String
            Return Nothing 'Function body goes here
        End Function

    End Class

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 MyClass1

        'Contract name must match!
        <Import()>
        Public Property MajorRevision As Func(Of Integer, String)
    End Class

For more information about how to use of the Func<T, T> object, see Func(Of 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 MyClass1

        <Import("TheString")>
        Public Property MyAddin

    End Class

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", GetType(IMyAddin))>
    Public Class MyLogger
        Implements IMyAddin

    End Class

    <Export("TheString")>
    Public Class MyToolbar

    End Class

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 MyClass1

        <Import()>
        Public Property MyAddin As Lazy(Of IMyAddin)

    End Class

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(GetType(IMyAddin))>
    Public Class MyLogger
        Implements IMyAddin

    End Class

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 MyClass1

        Private _theAddin As IMyAddin

        'Default constructor will NOT be used
        'because the ImportingConstructor
        'attribute is present.
        Public Sub New()

        End Sub

        'This constructor will be used.
        'An import with contract type IMyAddin
        'is declared automatically.
        <ImportingConstructor()>
        Public Sub New(ByVal MyAddin As IMyAddin)
            _theAddin = MyAddin
        End Sub

    End Class

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 Sub New(<Import(GetType(IMySubAddin))> ByVal MyAddin As IMyAddin)

        End Sub

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 MyClass1

        <Import(AllowDefault:=True)>
        Public Property thePlugin As Plugin

        'If no matching export is available,
        'thePlugin will be set to null.
    End Class

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 MyClass1

        <ImportMany()>
        Public Property MyAddin As IEnumerable(Of IMyAddin)
    
    End Class

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 MyClass1

        <ImportMany()>
        Public Property MyAddin As IEnumerable(Of Lazy(Of IMyAddin))
    
    End Class

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.
    End Class

    <Export()>
    Public MustInherit Class DataTwo
        'This part will not be discovered
        'by the catalog.
    End Class

    <PartNotDiscoverable()>
    <Export()>
    Public Class DataThree
        'This part will also not be discovered
        'by the catalog.
    End Class

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

        ReadOnly Property Name As String

        <DefaultValue(1)>
        ReadOnly Property Version As Integer

    End Interface

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(GetType(IPlugin))>
    <ExportMetadata("Name", "Logger")>
    <ExportMetadata("Version", 4)>
    Public Class MyLogger
        Implements IPlugin

    End Class

    'Version is not required because of the DefaultValue
    <Export(GetType(IPlugin))>
    <ExportMetadata("Name", "Disk Writer")>
    Public Class DWriter
        Implements IPlugin

    End Class

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 Property plugin As Lazy(Of IPlugin, IPluginMetadata)
    End Class

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 Property plugins As IEnumerable(Of Lazy(Of IPlugin, IPluginMetadata))

        Public Function InstantiateLogger() As IPlugin

            Dim logger As IPlugin
            logger = Nothing

            For Each Plugin As Lazy(Of IPlugin, IPluginMetadata) In plugins
                If (Plugin.Metadata.Name = "Logger") Then
                    logger = Plugin.Value
                End If
            Next
            Return logger
        End Function

    End Class

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 Property MyData As IMyData
    End Class

    Public Class NumTwo
        Inherits 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!

    End Class

    <InheritedExport()>
    Public Class NumThree

        <Export()>
        Public Property MyData As IMyData

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

    End Class

    Public Class NumFour
        Inherits 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.

    End Class

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(GetType(IPlugin))>
    <ExportMetadata("Name", "Logger")>
    <ExportMetadata("Version", 4)>
    Public Class Logger
        Implements IPlugin

        'Exports with contract type IPlugin
        'and metadata "Name" and "Version".
    End Class

    Public Class SuperLogger
        Inherits Logger

        'Exports with contract type IPlugin and 
        'metadata "Name" and "Version", exactly the same
        'as the Logger class.

    End Class

    <InheritedExport(GetType(IPlugin))>
    <ExportMetadata("Status", "Green")>
    Public Class MegaLogger
        Inherits Logger

        'Exports with contract type IPlugin and 
        'metadata "Status" only. Re-declaring 
        'the attribute replaces all metadata.

    End Class

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
        Inherits ExportAttribute

        Public Property MyMetadata As String

        Public Sub New(ByVal myMetadata As String)
            MyBase.New(GetType(IMyAddin))

            myMetadata = myMetadata
        End Sub

    End Class

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(GetType(IMyAddin))>
        <ExportMetadata("MyMetadata", "theData")>
        Public Property myAddin As MyAddin

        <MyAttribute("theData")>
        Public Property myAddin As MyAddin
[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.
    End Class

    Public Class PartTwo

        <Import()>
        Public Property partOne As PartOne

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

    End Class

    Public Class PartThree

        <Import(RequiredCreationPolicy:=CreationPolicy.Shared)>
        Public Property partOne As PartOne

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

    End Class



    <Export()>
    <PartCreationPolicy(CreationPolicy.NonShared)>
    Public Class PartFour
        'The NonShared creation policy is explicitly specified.
    End Class

    Public Class PartFive

        <Import()>
        Public Property partFour As PartFour

        '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.

    End Class

    Public Class PartSix

        <Import(RequiredCreationPolicy:=CreationPolicy.NonShared)>
        Public Property partFour As PartFour

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

    End Class

    Public Class PartSeven

        <Import(RequiredCreationPolicy:=CreationPolicy.Shared)>
        Public Property partFour As PartFour

        '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.

    End Class

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