Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Named Loggers with MEF

5.00/5 (3 votes)
23 Nov 2013CPOL2 min read 12.7K   134  
How to inject named logger using MEF

Introduction

This tip shows how you can import logger instance using MEF while the logger's constructor takes name of the target class as parameter.

Background

I started exploring MEF early this year as we are using it in our new project at work. We import all dependencies with the exception of loggers. We are using log4net and are creating loggers per class:

C#
private readonly ILog log = LogManager.GetLogger(typeof(MyClass));

The problem is how to achieve this with MEF. It seems impossible to pass target class’ name into the constructor. After much Googling, I found out that people usually settle with importing a log factory and then either passing the class name or walking the call stack. But with convention-based part registrations in MEF 2, there is another way.

Using the Code

Let’s say we want to import loggers to a property of type ILog. We create a convention for all types that contain such a property:

C#
var conventions = new RegistrationBuilder();

var typesWithLogProperty = conventions.ForTypesMatching(
    t => t.GetProperties().Any(p => p.PropertyType == typeof(ILog)));

We then embed type name in the import contract. The contract will start with a common prefix so we can identify it later:

C#
const string prefix = "log4net.ILog~";

typesWithLogProperty.ImportProperties(
    p => p.PropertyType == typeof(ILog),
    (p, builder) => builder.AsContractName(prefix + p.DeclaringType.FullName));

Then, we create a catalog which will use our new convention:

C#
var catalog = new AssemblyCatalog(typeof(Program).Assembly, conventions);

The import side is ready, now we need to create the export side. We will use a custom ExportProvider to create the instances. I tried to use FactoryExportProvider from MefContrib but it didn’t work for me. Here is my own simplified implementation:

C#
public class FactoryExportProvider : ExportProvider
{
    private readonly Func<string, object> _factoryMethod;

    public FactoryExportProvider(Func<string, object> factoryMethod)
    {
        _factoryMethod = factoryMethod;
    }

    protected override IEnumerable<Export> 
    GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
    {
        var value = _factoryMethod(definition.ContractName);
        if (value != null)
        {
            yield return new Export(definition.ContractName, () => value);
        }
    }
}

Finally, we create the log factory and add it to the composition container along with our catalog:

C#
var logFactory = new FactoryExportProvider(
    contractName => contractName.StartsWith(prefix)
        ? LogManager.GetLogger(contractName.Substring(prefix.Length))
        : null
    );

var container = new CompositionContainer(catalog, logFactory);

Every instance resolved using the container that has a property of type ILog gets a logger imported automatically, no attributes needed.

C#
[Export(typeof(Part))]
public class Part
{
    public ILog Log { get; set; }
}

var part = container.GetExport<Part>().Value;
part.Log.Info("Hello, world!");

To make it nicer to use, you can wrap the whole functionality in an extension method like this:

C#
public static ExportProvider ImportPropertiesEx<PropertyType>(
    this RegistrationBuilder conventions,
    Func<System.Reflection.PropertyInfo, string> getContractName,
    Func<string, PropertyType> createInstance)
    where PropertyType: class
{
    Type propertyType = typeof(PropertyType);
    string prefix = propertyType.FullName + "~";

    var typesWithLogProperty = conventions.ForTypesMatching(
        t => t.GetProperties().Any(p => p.PropertyType == propertyType));

    typesWithLogProperty.ImportProperties(
        p => p.PropertyType == propertyType,
        (p, builder) => builder.AsContractName(prefix + getContractName(p)));

    return new FactoryExportProvider(
        contractName => contractName.StartsWith(prefix)
            ? createInstance(contractName.Substring(prefix.Length))
            : null
        );
}

Points of Interest

I noticed that combining attribute exports and convention based exports confuses MEF so it prints warning in the output window.

In the real application, you would probably use a logger facade to avoid dependency on particular logging framework.

This approach could be easily adapted to work with constructor imports.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)