MEF Logger – 使用 MEF 创建插件






4.92/5 (12投票s)
在本文中,我将展示如何使用 MEF 构建一个可插入组件。
引言
目标是以非常简单的方式学习如何使用 MEF。让我们构建一个记录日志的组件,这在应用程序中非常重要,并且该组件应该以不同的方式记录日志。例如,记录到数据库、Windows 事件日志、文件服务器等。可插入组件的最大优点是,您无需修改应用程序即可添加其他日志记录器,这要归功于 MEF 团队,这非常简单。
背景
MEF 允许我们使用插件的概念。它提供了一个框架,允许我们指定应用程序可以扩展的点,公开可以被外部组件插入的模块。MEF 不会显式引用应用程序中的组件,而是允许我们的应用程序在运行时通过部件的组合来查找组件,并管理这些扩展的维护工作。因此,我们的应用程序不依赖于实现,而是依赖于抽象,并且可以在不重新编译甚至不中断执行的情况下为程序添加新功能。
MEF 架构(非常简单...)
要使用 MEF 构建可插入的应用程序,我们必须遵循以下步骤
- 定义扩展点
- 为每个扩展点定义 MEF 合同
- 定义一个类来管理扩展点
- 创建插件
- 定义一个目录
扩展点是我们希望允许扩展的应用程序的“部件”。在我们的示例中,扩展点是日志写入。
对于每个扩展点集,我们需要定义一个 MEF 合同,该合同可以是委托或接口。在我们的示例中,我们将创建一个包含定义我们的 MEF 合同的接口的 DLL。
为了告知 MEF 如何管理我们的插件,我们使用 `Import` 属性。在我们的示例中,`PluginHandler` 类将管理扩展。
要创建扩展,我们首先应该为所需的扩展点实现 MEF 合同。此扩展称为 MEF 可组合部件。要将一个类定义为可组合部件,我们使用 `Export` 属性。
目录在运行时保存所有导入的可组合部件的列表。目录可以是 `AssemblyCatalog`、`DirectoryCatalog`、`TypeCatalog` 和 `DeploymentCatalog` 类型。在我们的示例中,我们使用 `DirectoryCatalog`,它会在特定目录中发现插件。
使用代码
我们的解决方案将包含五个项目。
项目名称 |
项目类型 |
description |
参考文献 |
MefLogger |
类库 |
主 DLL,供您的应用程序使用 |
MefLogger.Interface |
MefLogger.Interface |
类库 |
供插件使用的 DLL |
|
MefFileLoggerPlugin |
类库 |
用于将日志注册到文本文件的插件 |
MefLogger.Interface |
MefEventLoggerPlugin |
类库 |
用于将日志注册到 Windows 事件日志的插件 |
MefLogger.Interface |
ApplicationTest |
控制台 |
测试应用程序 |
MefLogger |
该解决方案基于以下概念
为了创建一个可插拔的系统,我们必须导出必须由插件实现的接口。为此,我创建了一个单独的项目来定义 `MefLogger.Interface` 接口。
MefLogger 项目是我们将使用 MEF 来导出和导入插件接口、管理和触发插件命令的组件。有趣的是,MefLogger 项目不知道如何注册日志;插件需要知道如何做到这一点。该项目是一个 DLL,它将查找和管理插件;这样,我们就无需在想要实现日志记录的每个应用程序中烦恼地实现扩展点。只需在我们的应用程序中引用 MefLogger DLL 即可。
MefFileLoggerPlugin 和 MefEventLoggerPlugin 项目是我们的插件。SampleApp 项目是一个非常简单的控制台应用程序,演示了 MefLogger DLL 的用法。好吧,让我们开始示例。
创建一个新的空白解决方案,并将其命名为 MefLoggerSolution,如图 1 所示。
通过右键单击解决方案(对于惯用右手者)来添加一个新的类库项目。将此项目命名为 MefLogger.Interface。该项目将包含我们插件的合同。
将 `Class1` 重命名为 `IMefLogger`。在名称开头使用字母“I”来标识类为接口是一个好习惯。
重命名类后,我们将收到以下消息
单击“是”按钮,Visual Studio 将把所有对 `Class1` 的引用替换为 `IMefLogger`。
修改类的内容,使其成为一个接口。代码应如下所示:
using System;
namespace MefLogger.Interface
{
public interface IMefLogger
{
void Log(string message);
}
}
向我们的解决方案添加一个新的类库项目。将此项目命名为 MefLogger。该项目将是我们的主 DLL。
将 Class 1 名称更改为 Logger。
向 MefLogger 项目添加一个名为 `PluginHandler` 的新类。该类将负责加载插件并向插件发送命令。
现在让我们添加必要的引用。由于我没有将 MEF 注册到 GAC,我将添加对位于某个文件夹中的 MEF DLL 的引用。
添加对 MefLogger.Interface 项目的引用。
还添加一个应用程序配置文件,并将其命名为 _MefLogger.dll.config_。
使用 DLL 中的配置文件并非易事。要实现这一点,我们必须遵循以下步骤
- 添加一个新的应用程序配置文件,并将其命名为 _MefLogger.dll.config_。
- 将 BuildAction 属性更改为 Content。
- 将 Copy to Output 属性更改为 Copy Always。
修改 _MefLogger.dll.config_ 文件,如下所示
<?xml version="1.0" encoding="utf-8" ?>
<conFiguretion>
<appSettings>
<add key ="PluginPath" value="D:\Artigos\MefLoggerSolution\Plugins"/>
</appSettings>
</conFiguretion>
现在我们可以编写代码来使用 MEF 和管理插件了。
将 `PluginHandler` 类修改为以下代码
using System;
using System.Collections.Generic;
// Referências ao MEF
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.ComponentModel.Composition.ReflectionModel;
using System.ComponentModel.Composition.AttributedModel;
//
using MefLogger.Interface;
using System.Reflection;
using System.ConFiguretion;
namespace MefLogger
{
internal class PluginHandler : IDisposable
{
// O atributo ImportMany permite que importemos várias classes que
// implementam a mesma interface
[ImportMany(typeof(IMefLogger))]
// AddinList armazenará todos os plugins carregados
public List<IMefLogger> AddinList
{ get; set; }
// AggregateCatalog armazena os catálogos do MEF
AggregateCatalog catalog = new AggregateCatalog();
public void InitializePlugins()
{
// cria a instância da nossa lista de plugins
AddinList = new List<IMefLogger>();
// cria um DirectoryCatalog e adiciona ao nosso AggregateCatalog
catalog.Catalogs.Add(new DirectoryCatalog(GetConFiguretionPath(), "*.dll"));
// cria o CompositionContainer
CompositionContainer cc = new CompositionContainer(catalog);
// Efetua a mágica.... É neste momento que o MEF
// carrega os plugins e os adiciona ao nosso AddinList
cc.ComposeParts(this);
}
public void WriteLog(string message)
{
// para cada plugin carregado em AddinList
foreach (IMefLogger l in AddinList)
{
// chama o método Log de cada plugin
l.Log(message);
}
}
private string GetConFiguretionPath()
{
/* Utilizar arquivos de conFigureção em DLL não é trivial.
* Para conseguir isso devemos seguire os seguintes passos:
* - Adicionar o arquivo App.Config
* - Mudar a propriedade BuildAction para Content
* - Mudar a propriedade Copy to Output para Copy Always
* - Alterar o nome de App.Config para MefLogger.dll.config
*/
// Abre o arquivo de conFigureção a partir da pasta onde a DLL está
ConFiguretion PluginConfig =
ConFiguretionManager.OpenExeConFiguretion(
this.GetType().Assembly.Location);
// Recupera a seção appSettings
AppSettingsSection PluginConfigAppSettings =
(AppSettingsSection)PluginConfig.GetSection("appSettings");
// retorna o valor da chave PluginPath
return PluginConfigAppSettings.Settings["PluginPath"].Value;
}
public void Dispose()
{
catalog.Dispose();
catalog = null;
AddinList.Clear();
AddinList = null;
}
}
}
现在,将 `Logger` 类修改为以下代码
singusing System;
namespace MefLogger
{
public class Logger : IDisposable
{
// Vamos utilizar o padrão Singleton,
// para evitar carregar os plugins várias vezes.
static Logger singletonLogger;
PluginHandler h;
private Logger()
{
h = new PluginHandler();
h.InitializePlugins();
}
// Implementação do padrão Singleton
public static Logger GetLogger()
{
// se a instância da classe Logger não foi criada, então cria.
if (singletonLogger == null)
singletonLogger = new Logger();
return singletonLogger;
}
public void Log(string message)
{
h.WriteLog(message);
}
public void Dispose()
{
h = null;
singletonLogger = null;
}
}
}
为了组织我们的解决方案,添加一个名为 _Plugins_ 的解决方案文件夹。此文件夹仅为逻辑的,因此如果您在 Windows 资源管理器中查看,将不会看到此文件夹。我们将使用此文件夹来创建插件项目。
在 _Plugins_ 文件夹中添加一个名为 TextLoggerPlugin 的新类库项目。在此项目中,我们将创建一个用于将日志写入文本文件的插件。添加对 MefLogger.Interface 项目和 System.ComponentModel.Composition.CodePlex.Dll MEF DLL 的引用。对于插件,这是唯一需要的 MEF DLL 引用。
usingusing System;
using System.ComponentModel.Composition;
using MefLogger.Interface;
using System.IO;
namespace TextLoggerPlugin
{
[Export(typeof(IMefLogger))]
public class TextLogger : IMefLogger
{
public void Log(string message)
{
// o arquivo será criado na pasta onde a aplicação SampleApp estará rodando.
StreamWriter sw = File.AppendText("addinlog.txt");
sw.WriteLine(message);
sw.Close();
}
}
}
现在让我们创建另一个插件项目,EventLoggerPlugin,用于将我们的日志记录到 Windows 事件日志中。
usingusing System;
using System.ComponentModel.Composition;
using MefLogger.Interface;
using System.Diagnostics;
namespace EventLoggerPlugin
{
[Export(typeof(IMefLogger))]
public class EventLogger : IMefLogger
{
string sSource = "";
string sLog = "";
string sEvent = "";
public void Log(string message)
{
sSource = "EventLoggerAddin";
sLog = "Application";
sEvent = message;
try
{
if (!EventLog.SourceExists(sSource))
EventLog.CreateEventSource(sSource, sLog);
EventLog.WriteEntry(sSource, sEvent);
EventLog.WriteEntry(sSource, sEvent, EventLogEntryType.Information, 234);
}
catch
{
// Ocorrerá erro caso não esteja rodando o programa com
// permissões de administrador
}
}
}
}
现在我们需要添加一个项目来测试我们的 DLL。让我们添加一个名为 SampleApp 的新控制台应用程序项目。
为了测试我们的应用程序,我们只需添加对 MefLogger 项目的引用。
将我们的 SampeApp 项目设置为“启动项目”。
将 `Program` 类修改为
usingusing System;
using MefLogger;
namespace SampleApp
{
class Program
{
static void Main(string[] args)
{
Logger _logger = Logger.GetLogger();
_logger.Log("Log Teste 1");
_logger.Log("Log Teste 2");
_logger.Log("Log Teste 3");
_logger.Dispose();
}
}
}
最后,我们的解决方案将如下所示
要运行该应用程序,我们必须创建在 _MefLogger.dll.config_ 文件中定义的文件夹,并将插件项目中创建的 DLL 复制到那里。
您已完成。
您可以修改这个简单的示例并根据您的需求进行调整。
结论
我们已经看到,MEF 非常易于使用,并且极大地简化了可插拔系统的开发。
关注点
这是 CodePlex 上的 MEF 网站:http://mef.codeplex.com/documentation。