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);
}
}
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
}
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);
}
}
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; }
}
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;
}
}
}
}
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;
}
}
}
}
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; }
}