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:
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:
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:
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:
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:
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:
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.
[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:
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.