Issues

An Introduction to MEF Applied in the Umbraco Real-World

It all starts at Codegarden…

This year I attended the Codegarden "confestival", as I have been doing for the last 7 editions. Plenty of interesting sessions to attend, great people from all over the world to hang out and discuss with, in other words, "business as usual" at Codegarden. This year's edition was particularly great in my opinion though, nonewithstanding the fact that it was held in a new venue, in a new city.

It is not the topic of this article, but for those interested, there have been some nice blog posts and articles written about Codegarden 16 that highlight the great job of Umbraco HQ and that I encourage you to read. Here are a few links of my choice:

Every year, I come back home from Denmark telling myself I need to find a way to contribute more actively to this great community, but then the normal work flow quickly takes over, I do a few things but before I know it, it is already next year's Codegarden edition and nothing happened.

The fact is that, somehow, this year's edition has been more inspiring to me on that regards. This article is in fact the result of the combination of three sessions I attended over the three days of the conference.

The first one was the "Umbraco anti-patterns" session by Marc Goodson (@marcemarc), where the "copy-paste" anti-pattern was brilliantly highlighted with actual code consisting of about a 100 (or was that more?) "if ... then" copy-paste statements.

The second session was "Cloudy with a chance of Umbraco Awesomeness" by Maff Rigby (@MaffRigby) , where, at one point, Maff explained to us that his solution contained a lot of different Document Types in order to create specific flows. This resulted in an easy way of working for the end-users, but it increased the complexity in the back-end code quite a lot.

I will skip the details (the whole session is available online at https://video.twentythree.net/cloudy-with-a-chance-of-umbraco-awesomeness for those of you interested), but the consequence of this is that, when doing custom CRUD operations when publishing content, at some point Maff had to do huge switch statements over the different document types, with lots of situations where the action to execute was in fact the same. I will come back on this point later on.

The third session that inspired me was an open-space session, on the last day, about Skrift, where Erica, Janae and Kyle basically told us they were looking for people to write articles, and that we should not be afraid to write. The discussion also went about the fact that sometimes people wanted to write but did not know what to write about, so some actions were taken to get an open Trello board to help out the candidate-authors on that matter.

At the end of the session, I decided I would try to write an article for Skrift, but I had no idea of a topic, so I would definitely check the list of potential topics when it would become available. In the meantime, the Trello board has been created at the following URL: https://trello.com/b/NdeqxML2/skrift-article-ideas .

Every year, I come back home from Denmark telling myself I need to find a way to contribute more actively to this great community

Now, as a matter of fact, I did not have to wait for the Trello board, because an idea of technical topic came to me on the last night in Odense, as I was thinking about the issue that Maff had described to us. It occurred to me that MEF might be a solution to such a problem.

The very basics of MEF

Basically, the idea with composition is that a class can declare itself as exposing a specific interface and that, at the other end, the developer can define a variable in which s.he wants to get access to one, or all, the classes that expose that specific interface.

What is MEF, you will ask? It is a composition framework provided by Microsoft, it stands for "Managed Extensibility Framework". My experience when talking to people about it, is that it is not really well known and not used that much, although I think it can be quite helpful in some situations, like the one faced by Maff. And I figured it might be interesting to share some thoughts about this.

So, here we go, let's talk about MEF! The idea of this article is not to give a full description of MEF's possibilities and functionalities. I will focus on handling situations like the one described to us in Maff's session. For full details about MEF, you can of course check the Microsoft documentation online.

Basically, the idea with composition is that a class can declare itself as exposing a specific interface and that, at the other end, the developer can define a variable in which s.he wants to get access to one, or all, the classes that expose that specific interface. There is of course the need to actually build the composition via a composer at some point, e.g. in the constructor.

What happens during the composition build, is that the composer will go through some specific folders (your application [bin] folder and also into other folders that you can specify explicitly), match all the classes that expose the specified interface, and assign an instance of them to the composition variable.

It is important to note that this composition is performed at assembly level (reflection through the compiled dll's), which means that MEF makes it possible to implement a kind of plug-in/plug-and-play mechanism in your application. Let us see how with a simple example

A first glance

Let us say you need to implement a logging mechanism in your application, which saves all logging information into a log file and also sends an email in case of error. There are of course zillions of ways to do this, but let us go the MEF way to describe the plumbing in a basic scenario.

 For the illustration, we will suppose that we create each class of the example below in a separate assembly, but you could as well have everything in the same assembly.

There is a fully functional version of the code below on my GitHub repository under https://github.com/mikecp/Skrift_MEF/tree/master/SkriftMEF/, all files related to this first example are located under the “Logging” subdirectory.

MEF namespaces

First of all, you need to include the namespaces for MEF:

  1. Add a reference to the assembly System.ComponentModel.Composition
  2. In your code, you will need to add references to the following namespaces:
System.ComponentModel.Composition;
System.ComponentModel.Composition.Hosting;

Composition interface

We start by creating the "composition interface" in the assembly Interface.dll:

public interface ILogger
{
    string Name { get; }
    void Log(int level, string message);
}

Composition classes

Then, we create a class to write the message to the log file, in a separate assembly, e.g. FileLogger.dll

[Export(typeof (ILogger))] // <== this class exposes the ILogger interface for composition
public class Log2File : ILogger
{
    // ILogger interface implementation
    public string Name
    {
        get { return "File Logger"; }
    }

    public void Log(int level, string message)
    {
        // Do some stuff
        Console.WriteLine("Logging following info to file: {0} - {1}", level, message);
    }
}

And now we create the class to send an email in case of error, yet in a separate assembly, e.g. MailLogger.dll

[Export(typeof (ILogger))] // <== this class exposes the ILogger interface for composition
public class Log2Mail : ILogger
{
    private int _minErrorLevel = 4;

    // ILogger interface implementation
    public string Name
    {
    	get { return "Email Logger"; }
    }

    public void Log(int level, string message)
    {
    	// If the error level exceeds a specified threshold, we send an email
 	if (level > _minErrorLevel)
 	{
     	    // Do some stuff to send an email
   	    Console.WriteLine("Sending an email with following info: {0} - {1}", level, message);
 	}
    }
}

Using the composition

In the class where we want to do the logging, we need to have the following code, yet in a separate assembly, e.g SkriftMEF.Logging.MyProgram.exe:

public class LoggingSample
{
    [ImportMany(typeof(ILogger))] // <== this variable will hold a composition of all classes implementing the ILogger interface. 
    IEnumerable _myLoggers;

    public LoggingSample()
    {
        var catalog = new AggregateCatalog();
 	catalog.Catalogs.Add(new DirectoryCatalog(@".\\"));  // root is the bin directory
 	catalog.Catalogs.Add(new DirectoryCatalog(@"..\\..\\LoggingPlugins")); // add a specific directory

 	CompositionContainer _container = new CompositionContainer(catalog);

 	try
 	{
   	    _container.ComposeParts(this); // <== this will add into the _myLoggers variable an instance of each class implementing ILogger that can be found in the folders contained in the composer catalog
        }
 	catch (CompositionException compositionException)
 	{
            Console.WriteLine(compositionException.ToString());
        }
    }

    // Method called by the main program
    public void DoSomeTests()
    {
        MyNotCriticalMethod();
        Console.WriteLine();
        MySuperCriticalMethod();
    }

    public void MyNotCriticalMethod()
    {
    	try
    	{
            Console.WriteLine("Doing some not critical logic");

            // Some non-critical logic happens here

            throw new Exception("Something non critical went wrong");
	}
        catch (Exception e)
        {
            LogException(1, e); // <== this is a low importance problem
        }
    }

    public void MySuperCriticalMethod()
    {
        try
        {
            Console.WriteLine("Doing some very critical logic");

            // Some very critical logic happens here

            throw new Exception("Something very critical went wrong");
        }
        catch (Exception e)
        {
            LogException(10, e); // <== this is really a critical issue!
        }
    }

    // Helper class for the logging
    private void LogException(int level, Exception e)
    {
        Console.WriteLine("Something went wrong");
        foreach (var logger in _myLoggers)
            logger.Log(level, e.Message);
    }
}

In the code above, we define the variable _myLoggers to hold the composition and then, in the constuctor of the class, we build the composition. Note that we use the attribute ImportMany which indicates that we expect to retrieve a collection of matching classes. If we expect only one class to match the composition, then we should use the attribute Import.
There is also a helper class for logging the exceptions, that basically calls the Log method on all instances of the classes in the composition.

The logical and expected outcome for this, is that for the exceptions in MyNotCriticalMethod, the error message will be saved in the log file on disk but no email will be sent (level 1 warning). In case of exceptions in MySuperCriticalMethod, the error message will be saved in the log file on disk and an email will be sent (level 10 error).

Nothing fancy so far, maybe a point to note is that we do not need to implement any logging logic in our main class per se, we just loop through the classes in _myLoggers and the logic is performed in the ILogger instances. Also, the main class project does not need to have any reference to the composition classes, I have just copied the compiled composition dll’s under the specific folder “LoggingPlugins”.

MEF Plug-and-Play

Now, as I said, the composition is done on assembly level, so let's do some plug and play on the example above with two scenarii!

Scenario 1

For some reason you do not want to send mails anymore, regardless of the error level. How do you achieve that? Well, because we have chosen to have all classes in a separate assembly, we need to remove the assembly MailLogger.dll from the directory it resides in, and that's about it.

You might need an app restart if you are not using the default (bin) directory. But your application will stop sending emails right away, because the class Log2Mail has disappeared and therefore it will not be included anymore in the composition. No extra development, compilation or configuration change is needed in the rest of your existing code.

You can try it for yourself by removing the assembly “SkriftMEF.Logging.MailLogger.dll” from the directory “LoggingPlugins” of the “MyProgram” project. If you then run it again, everything still works and you do not get any output from the mailing assembly anymore.

Scenario 2

For some reason, you want to log the super critical errors in your Windows Event Log. Well, you only need to create a new class Log2EventHandler similar to the Log2Mail we described earlier, which performs some actions when the level is, for example, above 9.

If you create this class in a separate assembly and copy it under the bin (or otherwise specified) directory, the exceptions occurring in MySuperCriticalMethod will start logging into the event handler immediately. Again, at no extra cost of development or configuration change in your already existing code.

Isn't that great?

From Switch to MEF

Now that we have seen a first example which sets the basic building blocks of MEF, let us see how this mechanism can be applied to Maff's "multi-switch" situation. Let us begin by clearly identifying our starting point in a generic way: somewhere in the code we have a few methods doing custom CRUD operations on content publishing, and the actual custom actions to perform depend on the document type. So we have some code that looks like this, and this code repeats for Update, Delete, etc.:

public class MyCustomCRUDClass
{
    public MyCustomCRUDClass()
    {}

    public void Create()
    {
        var myCustomDAL = new MyCustomDAL(); // some custom data access layer
        switch (node.DocType)
        {
            case "DocType1":
            case "DocType2":
            …
            case "DocType25":
                myCustomDAL.CreateMethod1(node, ...);
                break;
            case "DocType26":
                myCustomDAL.CreateMethod2(node, ...);
                break;
            case "DocType27":
            case "DocType28":
                myCustomDAL.CreateMethod3(node, ...);
                break;
            case "DocType29":
            case "DocType30":
            ...
            case "DocType40":
                myCustomDAL.CreateMethod4(node, ...);
                break;
            ...
        }
    }
}

I think we can all agree that this code will not be easy to maintain, if for example a new DocType is created which must be included in the switch check, or if a DocType is renamed for some reason, etc.

Let us see how we can use MEF here to improve the maintainability/scalability of the code. A fully functional version of the code below is also available on the GitHub repository, under the “SwitchToMEF” subdirectory.

Object model

Because it would be hard to simulate the Umbraco environment, I have created a simple object model to “mimic” a content node:

public class ContentNode
{
    public ContentNode(string name, string documentType)
    {
        Name = name;
        DocumentType = documentType;
    }
    public string Name { get; private set; }
    public string DocumentType { get; private set; }
}

Composition interface

First of all, let us define the composition interface for our CRUD operations:

public interface IMyCRUDOperations
{
    IEnumerable SupportedDocTypes { get; }
    void Create(ContentNode node);
    void Update(ContentNode node);
    void Delete(ContentNode node);
}

The point of attention here, is the property "SupportedDocTypes" in which the implementing class will need to specify the list of document types that it supports.

Composition classes

Now, for each "variation" of the Create method in the switch we used as example, we need to create a specific composition class that looks like the following:

[Export(typeof(IMyCRUDOperations))] // <== we expose the IMyCRUDOperations interface for composition
public class MyCRUDOperationsVariation1 : IMyCRUDOperations
{
    private IEnumerable _supportedDocTypes = new[] { "DocType1", "DocType2", /*... ,*/ "DocType25" };
    private MyCustomDAL  _myCustomDAL = new MyCustomDAL(); // some custom data access layer    

    // Implementation of the IMyCRUDOperations interface
    public IEnumerable SupportedDocTypes { get { return _supportedDocTypes; } }  //<== This implementation supports doc types 1 to 25

    public void Create(ContentNode node)
    {
        _myCustomDAL.CreateMethod1(node);
    }

    public void Update(ContentNode node)
    {
        _myCustomDAL.UpdateMethod1(node);
    }

    public void Delete(ContentNode node)
    {
        _myCustomDAL.DeleteMethod1(node);
    }
}

I have not done the whole list here, but I guess you get the idea for doc types 26 to 40: we get 3 extra variations, their full implementation is available in the GitHub files (this time I have put all in the same assembly).

Switch to MEF

Finally, let us see how we can transform the switch statement in our main program, SkriftMEF.SwithToMEF.Program.

public class CRUDSample
{
    [ImportMany(typeof(IMyCRUDOperations))]  // <== This variable will hold a composition list
    IEnumerable _myCRUDVariations;

    public CRUDSample()
    {
        var catalog = new AggregateCatalog();
        catalog.Catalogs.Add(new DirectoryCatalog(@".\\"));  // root is the bin directory
        catalog.Catalogs.Add(new DirectoryCatalog(@"..\\..\\CRUDVariations")); // add a specific directory

        CompositionContainer _container = new CompositionContainer(catalog);

        try
        {
            _container.ComposeParts(this); // <== this will add into the _myCRUDVariations variable an instance of each class implementing IMyCRUDOperations that can be found in the folders contained in the composer catalog
        }
        catch (CompositionException compositionException)
        {
            Console.WriteLine(compositionException.ToString());
        }
    }

    public void Create(ContentNode node)
    {
        // Those few lines of code replace the whole switch statement
        var curDocType = node.DocumentType; // In Umbraco world, this would be a different call to get the doc type of your node
        var curVariation = _myCRUDVariations.FirstOrDefault(variation => variation.SupportedDocTypes.Contains(curDocType));  // search for the variation handling this node's document type
        if (curVariation == null)
            Console.WriteLine("ERROR : No CRUD variation found for document type " + curDocType);
        else
            curVariation.Create(node);
    }
}

In the code above, we initialize the composition as seen in the first example. Then, in the Create (or Update, or Delete, ...) method, we look in the composition list of IMyCRUDOperations, _myCRUDVariations, and try to find an instance that supports the content type of the node being created (or updated, or deleted). This happens in the "FirstOrDefault" Linq statement. If we do find an instance, we just call the Create method on it, and voilà! Isn't that cool?

Benefits of the MEF solution

This MEF implementation bring us some nice benefits, among which I would like to point out the following:

  • In the main part of the code, we have reduced the 50+ lines of the "switch/copy-paste" code to 5 lines of "generic" code, which reduces the risk of errors and makes the code more readable and maintainable.
  • We have removed the actual logic from the switch statement and encapsulated it into the individual implementations of the interface IMyCRUDOperations.
  • Even if we still need to hardcode the doctype names somewhere in the code, it is now centralized in one place for each doc type. Let us not forget that we probably have the same situation for Update and Delete, so a duplication of the switch statements, meaning a duplication of the risk to make a mistake in the doctype names. With MEF, we basically have a duplication of the 5 "generic" lines of code, where we just need to change the method called on the curVariation variable.

    Also, the order of the hardcoded doctype names is decoupled from the logic, which is good. What I mean is that, in the switch statement, the order of the doctype names has all its importance, and putting a doctype name too high or too low in the switch can have a major impact on what happens next in the business logic. In the MEF implementation, the doctype names are just put in a list in which we do a string search, so the order has no impact on what happens next, as long as the doc type names are set in the correct implementation of the IMyCRUDOperations interface.
  • If a new doctype has to be managed, or a document type must be deleted/renamed, the only thing to do is adapt the list of SupportedDocTypes in the correct variation implementation. And like I said just before, the position of the doc type name in the list has no importance.
  • If a new doctype brings in a new behaviour, the only thing left to do is to create a new implementation of the IMyCRUDOperations interface, expose it for composition, add the new doctype name in the SupportedDocTypesList and it will work right away. Also, as I mentioned earlier, if you implement this new version in a separate assembly, you simply need to copy the new dll under your bin or under your specific “CRUDVariations” folder without having to modify or recompile the rest of your code.

Have a try

That's about all folks! I hope this introduction to MEF, applied to a practical issue, has brought into light the kind of scenario where using a composition framework might be of interest. I have created the fully working solution on GitHib so that you can quickly try for yourself and play with it, see what happens when you remove the assemblies or if you add custom ones, etc.

I guess the main challenge is to get the understanding of the composition/plug-in architecture that MEF empowers, and from there on to keep it in mind and identify the situations in your work where it could come handy.

Also, like I said in the introduction, this article illustrates only one aspect of MEF. I have personally used it in other situations, like for example for displaying a list of available data export modules (export to Excel, to XML, ...) from which the end user can choose. By using MEF, adding or removing an export option, and handling the actual export, is just about adding or removing a dll in the bin directory.

There are plenty of ways to use MEF, so go check it out, try it, and get inspired :-) !!

Michaël Latouche

Michaël is a freelance web developer based in Brussels, Belgium. He has been building web sites for 20+ years, most recently as technical lead and lead developer on several important websites for the Belgian railways.

Michaël has been following Umbraco since 2009 and he is a 3x Umbraco MVP. Over the years, he has been contributing whenever he could and, in 2020, he joined the Umbraco Core Collaborators Team where he reviews and merges contributions from the Umbraco Community all around the world. He was a speaker at Codegarden 21 and 22, Hackathon co-host at Codegarden 23, and he is also co-organizer of the Belgian Umbraco User Group meetups.

In his spare time, you will find him following his sons' basketball teams, working around the family house or enjoying a family bicycle ride.

comments powered by Disqus