De-mystifying Meta-data in the Managed Extensibility Framework
One of the features in the Managed Extensibility Framework is the ability to associate metadata with the imports, exports and even the part that carries them. Metadata is commonly referred to as “data about data” and in the case of MEF it enables you to make run-time decision on the things that have been dynamically discovered by the container.
Not only does the metadata support in MEF enable you to make run-time decisions, but it also enables you to set up constrains on imports by telling the container what metadata that exports has to satisfy in order to be accepted. Metadata import constraints does not only enable you to reject exports with incomplete metadata, but it also enables you to do things like partitioning your exports based on available metadata.
The single most important thing to be aware of with regards to the MEF metadata support is that, internally, all metadata is stored in a Dictionary<string, object> structure. Using such a trivial implementation internally, MEF opens up for some pretty interesting things to be done with metadata.
So let’s see what sort of things that can be done.
Adding metadata to exports
The easiest way to add metadata to an export is to decorate it with the ExportMetadata attribute. The attribute can be applied to all type of exports (types, fields, properties and methods) and takes two parameters; a string key and an object value. The attribute can be applied multiple times to add more than one piece of metadata to the export.
[ExportMetadata("AuthenticatedUsersOnly", false)] [ExportMetadata("Priority", Priority.Low)] [Export(typeof(IExtension))] public class DefaultExtension : IExtension { public DefaultExtension() { } }The DefaultExtension export has two pieces of metadata associated with it; a boolean that indicates whether or not it should be available only to authenticated users or not, and an enum value indicating the priority of the extension. None of these are MEF specific and have been created for the demonstrative purpose of this blog post. You can add any key/value pair that you like, but they have to conform to the normal rules of values in attributes.
Accessing metadata on imports
To be able to tap into the available metadata for imported values, you have to let the MEF container know that you are interested in using it. There is a snag here though. You can only access import metadata if you have lazy imports. The reason for this is quite obvious if you stop to think about it for a moment. If you were to directly import instances, then how would you get the additional information passed to you? You need a container that can contain both a way to get to the actual instance and also the metadata that is associated with it.
So in order to get access to import metadata you could modify an import looking like the one below
[Import(typeof(IExtension))] public IExtension Extension { get; set; }into the following one
[Import(typeof(IExtension))] public Lazy<IExtension, Dictionary<string, object>> Extension { get; set; }Remember how I said that metadata is really just a dictionary with string keys and object values? Here we are telling MEF that we would like to get access to that dictionary for the import in question. The Lazy<T, K> type is a special version of Lazy<T> class created by the MEF team and it contains a property called Metadata, which has the property type defined by the type parameter K, so in this case it will be of the type Dictionary<string, object>>.
A call to
this.Extension.Metadata
will give you access to the normal members of any Dictionary<string, object> instance. Armed with this, you are now ready to make run-time decisions on the imported values! Simple, is it not? And there is more to come!
Moving from a weakly-typed to a strongly-typed reading experience
The biggest weakness so far is the fact that we are accessing metadata on a weakly-typed dictionary. Not only do we have to make sure that a certain piece of metadata is available before we use it, or risk throwing an exception, but we also have to perform explicit casts because all values are stored as objects.
As if the two drawbacks mentioned above were not enough, there is a third drawback to the approach. By accessing metadata using string keys we are introducing magic strings, a horrible code smell that is going to make maintenance of the codebase awful in the long run and it is not very refactor-friendly either!
Luckily MEF has the ability to overcome all these things using a very simple concept; metadata contracts. A metadata contract is nothing more than an interface with read-only properties. How does this improve the way you work with metadata? Each read-only property in the metadata contract is strongly-typed and the name of the property is the name of the metadata item!
Internally MEF will still represent metadata using a Dictionary<string, object> instance. However, because the framework uses structural typing it can inspect the metadata contract and determine if the contents of the dictionary satisfies it.
So let us create a metadata contract for the metadata that was added to the DefaultExtension earlier.
public interface IExtensionMetadata { bool AuthenticatedUsersOnly { get; } Priority Priority { get; } }Yes, it is that simple! The next thing to do is to modify the import so that it lets MEF know that instead of getting back the raw metadata dictionary, we want to get back a metadata view that matches the metadata contract
[Import(typeof(IExtension))] public Lazy<IExtension, IExtensionMetadata> Extension { get; set; }Now if you call
this.Extension.Metadata
you will gain access to two, strongly, typed properties; AuthenticatedUsersOnly and Priority, the same two properties that was defined on the IExtensionMetadata interface. This greatly simplifies the consumption of the metadata since there no longer is a need to verify that the metadata key is available or to cast the values before you use them! We have also gotten rid of the magic strings and if either of these properties are renamed you will get a compiler error if the name is not updated everywhere it is accessed!
Defining metadata requirements on imports
You may or may not have realized it yet but we have done all the work that is needed to setup metadata requirements on imports. Using the metadata contract, MEF will reject all exports that do not contain all the metadata that is needed to satisfy the metadata contract.
Say you wanted to make the AuthenticatedUsersOnly metadata optional. This would not be a problem if you accessed metadata using the dictionary; the key/value pair would simply be missing. However with metadata contracts the entire export is rejected, so how can we get around this and implement optional metadata requirements?
Simple, we use the DefaultValue attribute. With this attribute we can tell MEF what default value to use in replacement for a missing item in the dictionary. So all properties that have been decorated with this attribute are considered optional, and all others are mandatory.
Moving from a weakly-typed to a strongly-typed writing experience
Up until this point all metadata that has been added to the DefaultExtension part have been done so using the ExportAttribute. It exhibits all the problems we had while consuming the weakly typed, magic string based, dictionary of metadata; it is weakly-typed (object values) and relays on magic string (metadata name strings).
So, how can we get the same strongly-typed, no magic string, based functionality when defining metadata? The answer to this is a custom metadata attribute. Luckily MEF provides the functionality to define your own metadata attributes, which enables you to get rid of the ExportMetadata attributes and the issues that comes with it.
To create a custom metadata attribute, you do just like you would do when creating a normal custom attribute you also decorate it with the MetadataAttribute (I know! Metadata for metadata. Enough to blow your mind!). The properties on the custom attribute will be used by MEF to populate the internal metadata dictionary, again with the property name as the dictionary key and the property value was the dictionary value. The difference with a metadata contract is that the properties can be read-write.
[MetadataAttribute] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Module, AllowMultiple = false)] public class ExtensionMetadata : Attribute { public ExtensionMetadata(bool authenticatedUsersOnly, Priority priority) { this.AuthenticatedUsersOnly = authenticatedUsersOnly; this.Priority = priority; } public bool AuthenticatedUsersOnly { get; set; } public Priority Priority { get; private set; } }There are a couple of key things to pay attention to.
- The attribute has been decorated with the MetadataAttribute; without it MEF will ignore it and no metadata will be added to the export
- The attribute usage is setup to be valid on all things that can be exported: types, fields, properties and methods
- The AllowMultiple value is set to false (more on this below)
Setting AllowMultiple to false is very important. Without it, MEF accepts that you add the attribute more than once on the same export. If you think about that for a moment you should realize that this means that the same metadata key could potentially contain more than one value. Makes sense, yeah? MEF handles this by storing arrays of values instead of single values in the dictionary, meaning the way you access is will be different. There are usages for this behavior but for the most part you only want a single value, so remember to set this to false unless you explicitly know what you are doing.
Putting this new attribute to use, the DefaultExtension will be defined as follows
[ExtensionMetadata(false, Priority.Low)] [Export(typeof(IExtension))] public class DefaultExtension : IExtension { public DefaultExtension() { } }Much nicer is it not? Strongly-typed and no magic strings. Not only that but by naming the attribute ExtensionMetadata we have setup an expectation on the developer using the attribute for what metadata we require to be supplied. Using the ExportAttribute there is no way to communicate this to the exporter other than sticking it in the documentation and hoping it will be read.
Conveying optional and mandatory metadata with custom attributes
Remember how we were able to decorate properties on the metadata contract to create optional metadata requirements on imports? The same thing can be done with the values on the custom metadata attributes. We can achieve this by removing the constructor parameter for the AuthenticatedUsersOnly property, setting the default value in the constructor body and making the property read-write.
[MetadataAttribute] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Module, AllowMultiple = false)] public class ExtensionMetadata : Attribute { public ExtensionMetadata(Priority priority) { this.AuthenticatedUsersOnly = false; this.Priority = priority; } public bool AuthenticatedUsersOnly { get; private set; } public Priority Priority { get; private set; } }Using the attribute and relying on the default value for the AuthenticatedUsersOnly property would look like this
[ExtensionMetadata(Priority.Low)] [Export(typeof(IExtension))] public class DefaultExtension : IExtension { public DefaultExtension() { } }If you instead wanted to provide a value for the property you would simply do:
[ExtensionMetadata(Priority.Low, AuthenticatedUsersOnly = true)] [Export(typeof(IExtension))] public class DefaultExtension : IExtension { public DefaultExtension() { } }Starting to get the idea of just how much more enjoyable the custom metadata attributes are to work with than the ExportMedata attribute?
A side note on the structural typing of metadata
Because of the usage of structural typing inside of MEF, you can combine custom metadata attributes with ExportAttribute. For example the following example sets the priority using the custom attribute and then adds the metadata information (about only being valid for authenticated users) using the ExportAttribute.
[ExtensionMetadata(Priority.Low)] [ExportMetadata("AuthenticatedUsersOnly", true)] [Export(typeof(IExtension))] public class DefaultExtension : IExtension { public DefaultExtension() { } }MEF will take all metadata and put it in the same dictionary before it uses structural typing and determines that it can satisfy the metadata requirements imposed on the import by the IExtensionMetadata metadata contract.
[Import(typeof(IExtension))] public Lazy<IExtension, IExtensionMetadata> Extension { get; set; }Neat, ey?
Combining custom metadata attributes with metadata contracts
Right now we are at risk of getting the required import metadata (defined by the IExtensionMetadata metadata contract) and the expected exported metadata (defined by the ExtensionMetadata attribute) out of sync with each other. How? What happens if a property is added on either one of them and we forget to add the corresponding property on the other one? Bad things could happen. We do not want bad things to happen so let us resolve (or at least minimize) this potential problem.
Since the ExtensionMetadata attribute is a regular class, we can have it implement the IExtensionMetadata interface! So if you alter the definition of the interface you are going to have to reflect the changes (at least for when you add new properties) on the attribute or you will get a compiler warning.
[MetadataAttribute] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Module, AllowMultiple = false)] public class ExtensionMetadata : Attribute, IExtensionMetadata { public ExtensionMetadata(Priority priority) { this.AuthenticatedUsersOnly = false; this.Priority = priority; } public bool AuthenticatedUsersOnly { get; set; } public Priority Priority { get; private set; } }A minor change that improved the overall design.
A side note on the metadata dictionary
If you have an import that retrieves metadata using the dictionary you may notice that there are some additional metadata items in there already. For exports MEF ,will add information about type identity - a sort of namespace for contract names - and for parts it will add metadata about the creation policy. Do not worry about these; they are required by MEF to function and they will not have any impact on your metadata contracts, thanks to structural typing.
That is all there is to it
That is all I have to say about MEF metadata for now. Being able to add metadata support in your application is a powerful thing and as you could see there is nothing difficult about it. With a bit of customization you can provide a much better metadata experience in your code and for the exporters, than using the default ExportAttribute and Dictionary<string, object> container.