Exports and Metadata

Declaring Exports explained the basics of parts exporting services and values. In some cases it’s necessary to associate information with exports for a variety of reasons. Commonly it’s used to explain about the capabilities of an specific implementation of a common contract. This is useful to allow imports to either constraint the export that can satisfy it, or to import all available implementations at the time and check their capabilities in runtime before using the export.

Attaching Metadata to an Export

Consider the IMessageSender service introduced earlier. Suppose we have a few implementations, and they have differences that may be relevant to the consumer of the implementations. For our example the transport of the message and whether is secure are important information for a consumer (importer).

Using ExportMetadataAttribute

All we have to do to attach this information is to use the [System.ComponentModel.Composition.ExportMetadataAttribute]:

public interface IMessageSender
{
    void Send(string message);
}

[Export(typeof(IMessageSender))]
[ExportMetadata("transport", "smtp")]
public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine(message);
    }
}

[Export(typeof(IMessageSender))]
[ExportMetadata("transport", "smtp")]
[ExportMetadata("secure", null)]
public class SecureEmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine(message);
    }
}

[Export(typeof(IMessageSender))]
[ExportMetadata("transport", "phone_network")]
public class SMSSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine(message);
    }
}
Public Interface IMessageSender
    Sub Send(ByVal message As String) 
End Interface

<Export(GetType(IMessageSender)), ExportMetadata("transport", "smtp")>
Public Class EmailSender
    Implements IMessageSender
    Public Sub Send(ByVal message As String) Implements IMessageSender.Send
        Console.WriteLine(message) 
    End Sub
End Class

<Export(GetType(IMessageSender)), ExportMetadata("transport", "smtp"), ExportMetadata("secure", Nothing)> 
Public Class SecureEmailSender
    Implements IMessageSender
    Public Sub Send(ByVal message As String) Implements IMessageSender.Send
        Console.WriteLine(message) 
    End Sub
End Class

<Export(GetType(IMessageSender)), ExportMetadata("transport", "phone_network")>
Public Class SMSSender
    Implements IMessageSender
    Public Sub Send(ByVal message As String) Implements IMessageSender.Send
        Console.WriteLine(message) 
    End Sub
End Class

Using a Custom Export Attribute

In order to do it more strongly typed than using the ExportMetadataAttribute, you need to create your own attribute and decorate it with [System.ComponentModel.Composition.MetadataAttribute]. In this example we also derive from ExportAttribute, thus creating a custom Export attribute that also specifies metadata.

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class MessageSenderAttribute : ExportAttribute
{
    public MessageSenderAttribute() : base(typeof(IMessageSender)) { }
    public MessageTransport Transport { get; set; }
    public bool IsSecure { get; set; }
}

public enum MessageTransport
{
    Undefined,
    Smtp,
    PhoneNetwork,
    Other
}
<MetadataAttribute(), AttributeUsage(AttributeTargets.Class, AllowMultiple:=False)> 
Public Class MessageSenderAttribute
    Inherits ExportAttribute
    Public Sub New()
        MyBase.New(GetType(IMessageSender)) 
    End Sub
    Public Property Transport() As MessageTransport
    Public Property IsSecure() As Boolean
End Class

Public Enum MessageTransport
    Undefined
    Smtp
    PhoneNetwork
    Other
End Enum

Above, the MetadataAttribute is applied to our custom export attribute. The next step is to apply the attribute to our IMessageSender implementations:

[MessageSender(Transport=MessageTransport.Smtp)]
public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine(message);
    }
}

[MessageSender(Transport=MessageTransport.Smtp, IsSecure=true)]
public class SecureEmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine(message);
    }
}

[MessageSender(Transport=MessageTransport.PhoneNetwork)]
public class SMSSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine(message);
    }
}

<MessageSender(Transport:=MessageTransport.Smtp)> 
Public Class EmailSender
    Implements IMessageSender
    Public Sub Send(ByVal message As String) Implements IMessageSender.Send
        Console.WriteLine(message) 
    End Sub
End Class

<MessageSender(Transport:=MessageTransport.Smtp, IsSecure:=True)> 
Public Class SecureEmailSender
    Implements IMessageSender
    Public Sub Send(ByVal message As String) Implements IMessageSender.Send
        Console.WriteLine(message) 
    End Sub
End Class

<MessageSender(Transport:=MessageTransport.PhoneNetwork)> 
Public Class SMSSender
    Implements IMessageSender
    Public Sub Send(ByVal message As String) Implements IMessageSender.Send
        Console.WriteLine(message) 
    End Sub
End Class

That’s all that is required on the export side. Under the hood, MEF is still populating a dictionary, but this fact becomes invisible to you.

Note: You can also create metadata attributes that are not themselves exports, by creating an attribute which is decorated with MetadataAttributeAttribute. In these cases the metadata will be added to Exports on the same member where the custom metadata attribute was applied.

Importing Metadata

Importers can access the metadata attached to the exports.

Using Strongly-typed Metadata

To access metadata in a strongly-typed fashion created a metadata view by definining an interface with matching read only properties (names and types). For our sample it would be an interface like the following:

public interface IMessageSenderCapabilities
{
    MessageTransport Transport { get; }
    bool IsSecure { get; }
}
Public Interface IMessageSenderCapabilities
    ReadOnly Property Transport() As MessageTransport
    ReadOnly Property IsSecure() As Boolean
End Interface

Then you can start importing using the type System.Lazy<T, TMetadata> where T is the contract type and TMetadata is the interface you’ve created.

[Export]
public class HttpServerHealthMonitor
{
    [ImportMany]
    public Lazy<IMessageSender, IMessageSenderCapabilities>[] Senders { get; set; }

    public void SendNotification()
    {
        foreach(var sender in Senders)
        {
            if (sender.Metadata.Transport == MessageTransport.Smtp && 
                sender.Metadata.IsSecure)
            {
                var messageSender = sender.Value;
                messageSender.Send("Server is fine");
                
                break;
            }
        }
    }
}
<Export()>
Public Class HttpServerHealthMonitor
    <ImportMany()>
    Public Property Senders() As Lazy(Of IMessageSender, IMessageSenderCapabilities)() 

    Public Sub SendNotification()
        For Each sender In Senders
            If sender.Metadata.Transport = MessageTransport.Smtp AndAlso sender.Metadata.IsSecure Then
                Dim messageSender = sender.Value
                messageSender.Send("Server is fine")

                Exit For
            End If
        Next sender
    End Sub
End Class

Using Weakly-typed metadata

To access metadata in a weakly-typed fashion, you import uisng the type System.Lazy<T, TMetadata> passing IDictionary<string,object> for the metadata. You can then access the metadata through the Metadata property which will be a dictionary.

Note: In general we recommend the strongly-typed method for accessing metadata, however there are systems that need to access the metadata in a dynamic fashion, which this allows.

[Export]
public class HttpServerHealthMonitor
{
    [ImportMany]
    public Lazy<IMessageSender, IDictionary<string,object>>[] Senders { get; set; }

    public void SendNotification()
    {
        foreach(var sender in Senders)
        {
            if (sender.Metadata.ContainsKey("Transport") && sender.Metadata["Transport"] == MessageTransport.Smtp && 
                sender.Metadata.ContainsKey("Issecure") && Metadata["IsSecure"] == true)
            {
                var messageSender = sender.Value;
                messageSender.Send("Server is fine");
                
                break;
            }
        }
    }
}
<Export()>
Public Class HttpServerHealthMonitor
    <ImportMany()>
    Public Property Senders() As Lazy(Of IMessageSender, IDictionary(Of String, Object))() 

    Public Sub SendNotification()
        For Each sender In Senders
            If sender.Metadata.ContainsKey("Transport") AndAlso sender.Metadata("Transport") = MessageTransport.Smtp AndAlso sender.Metadata.ContainsKey("Issecure") AndAlso sender.Metadata("IsSecure") = True Then
                Dim messageSender = sender.Value
                messageSender.Send("Server is fine")

                Exit For
            End If
        Next sender
    End Sub
End Class

Metadata filtering and DefaultValueAttribute

When you specifiy a metadata view, an implicit filtering will occur to match only those exports which contain the metadata properties defined in the view. You can specify on the metadata view that a property is not required, by using the System.ComponentModel.DefaultValueAttribute. Below you can see where we have specified a default value of false on IsSecure. This means if a part exports IMessageSender, but does not supply IsSecure metadata, then it will still be matched.

public interface IMessageSenderCapabilities
{
    MessageTransport Transport { get; }
    [DefaultValue(false)];
    bool IsSecure { get; }
}
Public Interface IMessageSenderCapabilities
    ReadOnly Property Transport() As MessageTransport
    <DefaultValue(False)> 
    ReadOnly Property IsSecure() As Boolean
End Interface

Last edited Aug 9, 2010 at 5:20 PM by haveriss, version 13

Comments

mchurchill Oct 30, 2012 at 1:42 AM 
If you allow multiple custom export attributes, keep in mind that in the case of your MyExportWithMetadata { string Metadata {g;s} }, your matching metadata view interface will look like: IMyMetaView { string[] { g; } }.

hype8912 Oct 19, 2011 at 1:07 PM 
I'd like to note that when looking up the Metadata key in the Export Metadata Dictionary that the Key is case sensitive. It would be nice if it wasn't case sensitive or gave an option to disable that but no where in the documentation did I find that the key lookups were case sensitive.

kayub Aug 11, 2010 at 10:59 PM 
OK, so it took me a few hours, but I figured it out. In your attribute class, it has to call the base constructor passing in the type of your metadata attribute: base(typeof(IMessageSenderCapabilities))

Furthermore, you cannot access the Imported property inside of the importer class' constructor, it will throw a TargetInvocation exception.

Hope this helps someone else.

kfitzie Nov 4, 2009 at 5:45 PM 
Why can't I have IEnumerable<IMessageSender, IMessageSenderCapabilities> ?

Eshva Aug 27, 2009 at 12:39 PM 
The same as previous: {{System.Lazy<T, TMetadata>}}

Eshva Aug 27, 2009 at 12:37 PM 
Are you sure you need so many different brackets in: {[{System.Lazy<T, TMetadata>}}] :)
I guess you need just [System.Lazy<T, TMetadata>].

nblumhardt Jul 30, 2009 at 3:29 PM 
Thanks for the correction.

danielearwicker May 13, 2009 at 9:15 PM 
In preview 5 I'm trying ExportCollection<MyDelegate> and my exports elsewhere are static functions that export MyDelegate. I get an "internal error" inside MEF, in ReflectionProperty.GetValue, Assumes.NotNull(this._getMethod); If I use IEnumerable<MyDelegate> it works fine, but then I can't get the metadata - at least, not the expected way. See http://mef.codeplex.com/Thread/View.aspx?ThreadId=56225 for workaround.

joshtwist May 8, 2009 at 4:12 PM 
Can we get this updated - I just spent a while looking for ImportRequiredMetadata (which has obviously been removed). Replacing this with an example showing the best way to achieve the same would be good.

NathanRidley Apr 28, 2009 at 12:15 PM 
ImportRequiredMetadata appears to have been completely removed in preview 5. How do we statically qualify the import now?

rodrigoCatalano Mar 31, 2009 at 10:17 PM 
With respect to the importation dynamic I believe that should not be the responsibility of the consumer to search in all and than match one or n, the catalog should be able to make one prefilter.