在MEF的宿主中,当我们通过Import声明导入的对象时,组装(Compose)的时候会创建该对象。例如:
interface ILogger
{
void Log(string message);
}
[Export(typeof(ILogger))]
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine("logger 1" + message);
}
}
class Host
{
[Import]
ILogger _logger = null;
public Host()
{
var catalog = new AssemblyCatalog(this.GetType().Assembly);
var container = new CompositionContainer(catalog);
//这儿会创建ConsoleLogger对象
container.ComposeParts(this);
_logger.Log("hello world");
}
}
有的时候,有些组件的创建开销比较大,但又不会立即使用。此时,我们希望通过延迟初始化的方式将其延迟到使用的时候创建,从而提高性能(常见的是提高启动速度)。MEF是支持这一模式的,我们只需要修改一下导入的声明形式即可。
[Import]
Lazy<ILogger> _logger = null;
这样,Logger就会延迟到第一次使用的时候创建了。
元数据MetaData
有的时候,对于同一个服务有多个提供者,我们需要从中选择一个使用。MEF提供了ImportMany来解决这一需求。
[Export(typeof(ILogger))]
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
[Export(typeof(ILogger))]
class DbLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
class Host
{
[ImportMany]
ILogger[] _logger = null;
public Host()
{
var catalog = new AssemblyCatalog(this.GetType().Assembly);
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
_logger.FirstOrDefault(i => i is DbLogger).Log("hello world");
}
}
此时,如果我们想使用延迟导入的时候,就会变成如下形式:
class Host
{
[ImportMany]
Lazy<ILogger>[] _loggerServices = null;
public Host()
{
var catalog = new AssemblyCatalog(this.GetType().Assembly);
var container = new CompositionContainer(catalog);
//这儿会创建ConsoleLogger对象
container.ComposeParts(this);
_loggerServices.FirstOrDefault(i => i.Value is DbLogger).Value.Log("hello world");
}
}
咋一看并没有什么问题,所有的Logger都是延迟创建的。但是仔细分析一下就会发现,要找到DbLogger的时候,必须遍历所有的_loggerServices,这个遍历会导致创建Logger。也就是说,使用第一个Logger的时候可能创建所有的Logger。
那么,如何实现我们只创建所需要的Logger呢? 这个时候就轮到元数据出场了,MEF中的元数据可以将一个数据附加到Export的服务对象中一并导出,从而可以通过元素据找到对应的服务。首先我们看看最终的效果吧:
public interface ILoggerData
{
string Name { get; }
}
class Host
{
[ImportMany]
Lazy<ILogger, ILoggerData>[] _logger = null;
public Host()
{
var catalog = new AssemblyCatalog(this.GetType().Assembly);
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
_logger.FirstOrDefault(i => i.Metadata.Name == "DB Logger").Value.Log("hello world");
}
}
这里首先声明了一个元数据类型的接口ILoggerData,然后,导入的对象变成了Lazy<ILogger, ILoggerData>,这个对象有一个属性为Metadata,它的类型就是刚才声明的ILoggerData。导出的ILogger对象是延迟创建的,而元数据不是延迟创建的。我们可以通过遍历ILoggerData来找到所需要的Logger对象,从而实现只创建所使用的Logger对象。
现在的问题是:如何在导出的时候声明相关的元数据。MEF提供了两种方式:
通过ExportMetadataAttribute标记声明
这种方式是在导出的服务的时候一并通过ExportMetaDataAttribute属性标记元素据:
[ExportMetadata("Name", "Console Logger")]
[Export(typeof(ILogger))]
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
[ExportMetadata("Name", "DB Logger")]
[Export(typeof(ILogger))]
class DbLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
ExportMetaDataAttribute有两个参数,Name和Value,Name为属性名称,Value为属性值。Compse的时候,MEF会先创建一个实现了元数据对象,然后将根据将Value值赋值给Name所对应的属性名称。
这么做虽然比较简单,但是它有两个弊端:
1. 属性名称不是强类型
这里我们必须字符串来给标志属性名称,它们之间并没有语法级的一致性检查,在缺少nameof运算符的现在,一旦元数据属性名称更改的话是非常容易出错的。
2. 如果元数据有多个值的话赋值显得非常累赘。
假如我们增加了一个Priority类型的属性,
public interface ILoggerData
{
string Name { get; }
int Priority { get; }
}
此时,必须对所有的导出对象都增加一个ExportMetadata标记,写出如下形式:
[ExportMetadata("Name", "DB Logger")]
[ExportMetadata("Priority", 3)]
当属性更多一点的话相信程序员们就会骂娘了。并且一旦某个Export对象漏写了,改对象就不会被导入。这个是一个运行时的错误,非常不容易排查的。
通过Attribute标记
这种方式可以通过一个Attribute来标记元数据:
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
class LoggerDataAttribute : Attribute, ILoggerData
{
public string Name { get; private set; }
public LoggerDataAttribute(string name)
{
this.Name = name;
}
}
[LoggerData("Console Logger")]
[Export(typeof(ILogger))]
class ConsoleLogger : ILogger, ILoggerData
{
public string Name { get; set; }
public void Log(string message)
{
Console.WriteLine(message);
}
}
[LoggerData("DB Logger")]
[Export(typeof(ILogger))]
class DbLogger : ILogger, ILoggerData
{
public string Name { get; set; }
public void Log(string message)
{
Console.WriteLine(message);
}
}
首先,声明一个LoggerDataAttribute,这个Attribute必须被MetadataAttribute标记。然后,在Export的对象前加上该LoggerDataAttribute,这样MEF导入的时候就会根据该LoggerDataAttribute创建元数据了。
值得一提的是,这里的LoggerDataAttribute本身并不需要实现ILoggerData接口,它是一个DuckType的约定,只需要实现元数据的属性即可。我这里实现该接口主要是为了让编译器保障元数据属性都有被准确实现。