In this post I assume you have read my first post on this subject. You can find the first post here. In the first part of this blog post, I explained briefly what MEF is and I showed how you can extend MEF with a custom ExportProvider, so that instead of configuring your exports with attributes, you could configure your exports with a XML file. The advantage of using a XML file over using attributes is that the application doesn’t need to be recompiled when your exports change. But when you change your XML file, the application may need to restart, or you could poll the file for any changes. In this post I’m going to show how you can make use of MEF’s recomposition features to recomposition on the fly when the content of the configuration XML file is changed.
MEF Recomposition
Well, what is MEF recomposition? Take a look the the following code snippet:
1: [Import(AllowRecomposition=true, AllowDefault=true)]
2: public IGreeter Greeter
3: {
4: set
5: {
6: _greeter = value;
7: }
8: }
If you’ve read my first blog post, all of this should be familiar, except the “AllowRecomposition=true” and the “AllowDefault=true”. The latter just means that when MEF can not find a suitable export to match this import, it will inject null instead of throwing an exception. I’ll come back the “AllowRecomposition” part in a minute. First take a look at the following code snippet:
1: static void Main(string[] args)
2: {
3: var container = new CompositionContainer(new LinqToXMLExportProvider(@"....exports.xml"));
4: Printer p = new Printer();
5: container.ComposeParts(p);
6: p.Print();
7: Console.WriteLine("Change the export configuration file and press a key!!!!!");
8: Console.ReadKey();
9: p.Print();
10: Console.ReadKey();
11: }
This should also be pretty familiar. Let’s go back to the “AllowRecomposition=true” part. When MEF is asked to satisfy all the imports of the Printer object on line 5, it records that the imports of the Printer object has been satisfied and MEF keeps a reference to the Printer object around thanks to the “AllowRecomposition=true” option specified on the Import attribute. When the exports change, MEF can now automatically satisfy the imports of the printer object again using the new exports. This happens automatically and you don’t have to call ComposeParts() again. When the configuration file is changed when hitting line 8 and after pressing a key, the second Print method should display something different based on how the configuration file was changed. Let’s take a look at how the LinqToXmlExportProvider class was enhanced to make this all possible.
LinqToXmlExportProvider
1: public class LinqToXMLExportProvider : ExportProvider
2: {
3: //Key is the contract name, value is the assembly qualified name of the implementing class....
4: private Dictionary<string, string> _mappings;
5: private FileSystemWatcher _watcher;
6:
7: public LinqToXMLExportProvider(string mappingFile)
8: {
9: _mappings = ParseFile(mappingFile);
10: _watcher = new FileSystemWatcher(Path.GetDirectoryName(mappingFile), Path.GetFileName(mappingFile));
11: _watcher.Changed += new FileSystemEventHandler(_watcher_Changed);
12: _watcher.NotifyFilter = NotifyFilters.LastWrite;
13: _watcher.EnableRaisingEvents = true;
14:
15: }
16:
17: void _watcher_Changed(object sender, FileSystemEventArgs e)
18: {
19: Dictionary<string, string> newMappings = ParseFile(e.FullPath);
20:
21: //Get the keys that are new in the export file....
22: List<ExportDefinition> newDefinitions = newMappings.Keys.Except(_mappings.Keys).Select(
23: (key)=>CreateExportDefintion(key)).ToList();
24: //Get the keys that are removed in the export file
25: List<ExportDefinition> removedDefinitions = _mappings.Keys.Except(newMappings.Keys).Select(
26: (key) => CreateExportDefintion(key)).ToList();
27:
28: // find the keys that may have changed values in the export file....
29: var result = from oldKV in _mappings
30: join newKV in newMappings on oldKV.Key equals newKV.Key
31: select new {OldKeyValue = oldKV, NewKeyValue = newKV};
32: foreach (var item in result)
33: {
34: if(item.OldKeyValue.Value != item.NewKeyValue.Value)
35: {
36: //If keys have changed value, the contract is both added and removed...
37: newDefinitions.Add(CreateExportDefintion(item.NewKeyValue.Key));
38: removedDefinitions.Add(CreateExportDefintion(item.OldKeyValue.Key));
39: }
40:
41: }
42:
43: _mappings = newMappings;
44: using (AtomicComposition atomic = new AtomicComposition())
45: {
46: OnExportsChanging(new ExportsChangeEventArgs(newDefinitions, removedDefinitions, atomic));
47: atomic.Complete();
48: }
49: OnExportsChanged(new ExportsChangeEventArgs(newDefinitions, removedDefinitions, null));
50: }
51:
52:
53: private ExportDefinition CreateExportDefintion(string contractName)
54: {
55: Dictionary<string, object> metaData = new Dictionary<string, object>();
56: metaData.Add(CompositionConstants.ExportTypeIdentityMetadataName, contractName);
57: return new ExportDefinition(contractName, metaData);
58: }
59:
60: private Dictionary<string, string> ParseFile(string mappingFile)
61: {
62: XElement mappings = XElement.Load(mappingFile);
63: return mappings.Elements().ToDictionary((el)=> el.Attribute("contract").Value,
64: (el=>el.Attribute("type").Value));
65: }
66:
67: protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
68: {
69:
70: List<Export> exports = new List<Export>();
71: string implementingType;
72: if (_mappings.TryGetValue(definition.ContractName, out implementingType))
73: {
74: Type t = Type.GetType(implementingType);
75: object instance = t.GetConstructor(Type.EmptyTypes).Invoke(null);
76: ExportDefinition exportDefintion = new ExportDefinition(definition.ContractName, new Dictionary<string, object>());
77: Export toAdd = new Export(exportDefintion, () => instance);
78: exports.Add(toAdd);
79: }
80: return exports;
81: }
Most of this class was covered in my first blog post, so I’ll only cover the additions to this class:
- Line 5: I need to keep an eye on the XML configuration file and get notified when it changes. I use the FileSystemWatcher class for this.
- Lines 10-13: This is the instantiation and configuring of the FileSystemWatcher. I’m not interested in all the events, only when the content of the file changes.
- Lines 17-50: This is the method where it all happens. This method is responsible for handle the event when the configuration file changes and it needs to notify MEF of the changed exports. Let’s take a look at it in more detail:
- Line 19: First thing to do when the file changes is to parse the file again into a new Dictionary of contractnames with implementing classes.
- Lines 22-26: I need to find out which contracts were removed (properties of classes with imports of these contracts need to be set to null) and I need to find out which contracts were added, so that imports that previously were injected with null can now be satisfied with a value. I use the LINQ Except() method to accomplish this. I immediately create ExportDefinitions out of these contracts using the CreateExportDefinition() method. We’re skipping a few lines to take a look at this method next.
- Lines 53-58: The CreateExportDefinition() method, it takes a contractname and creates an ExportDefinition for it. An ExportDefinition contains a contractname and metadata. It took me a while to figure it out, but for the recomposition to work the metadata has to have at least one key / value pair. The key is the constant string defined in the “CompositionConstants.ExportTypeIdentityMetadataName” variable and the value is simply the contractname. It’s of great importance that this key / value pair is present in the metadata or else you can spend your whole day debugging wondering why nothing is happening :).
- Lines 28-31: I also need to find the contractnames that were present in both old exports and the new exports. I do this by joining the old mappings with the new mappings (LINQ’s standard join is an inner join).
- Lines 31-41: Iterating over the joined mappings to find out if the implementing class belonging to the contractname has changed. If so, it’s both added to the removed and the added collections.
- Lines 44-48: To let MEF know that the Exports have changed and it needs to recomposition, I need to fire the ExportsChanging event of the ExportProvider. I can use the inherited OnExportsChanging method for this on line 46. This method accepts an ExportChangeEventArgs that needs two IEnumerables, one of the removed exports and one of the added exports. You can see that the call to the OnExportsChanging() method is wrapped in an AtomicComposition in a using statement. You can compare the AtomicComposition to the TransactionScope class. Recompositioning happens in a transaction and can be rolled back using the AtomicComposition object. When everything went ok, you call the Complete() method.
- Line 49: After everything went ok you can fire the ExportsChanged event. This isn’t really needed for recomposition but maybe your application wants to notify the user that the functionality of your application has changed. To make this possible you need to fire the ExportsChanged event by using the inherited OnExportsChanged method, supplying the same arguments as to the OnExportsChanging() method. Since this event does nothing for recomposition, it doesn’t need a transaction and null is supplied for the AtomicComposition.
Concluding
Well, that’s it! This is all that is needed to let your custom ExportProvider know to MEF that recomposition is needed. Just imagine the possibilities, automatically recompositioning when you receive an email or a message from a message queue, they are endless. You can find a full working solution of the code above, here.