MEF(可扩展框架)使用总结
一般情况下,我们对系统要求:
1.对扩展开发对修改关闭
2.高层模块不应该依赖于低层模块,应该依赖于抽象
实际上,这是遵循了面向对象的设计原则中的开放封闭原则和依赖倒置原则,其所做的事情就是为了提高系统的可扩展能力和对代码解耦。
为了满足上述的要求,我们引用了微软的MEF框架,让他来帮助我们做这些事情。
一、概念
MEF (Managed Extensibility Framework) 使开发人员能够在其 .NET 应用程序中提供挂钩,以供用户和第三方扩展。可以将 MEF 看成一个通用应用程序扩展实用工具。
MEF 使开发人员能够动态创建扩展,无需扩展应用程序,也不需要任何特定于扩展内容的知识。这可以确保在编译时两者之间不存在耦合,使应用程序能够在运行时扩展,不需要重新编译。MEF 还可以在将扩展加载到应用程序之前,查看扩展程序集的元数据,这是一种速度更快的方法。
二、原理
将被我们特殊标记的方法,属性,字段,类型,导入到一个容器中,我们再通过MEF内置的反射来使用容器中被我们导入的各种方法,属性,字段,类型。
三、示例
1.类型
public interface IDBConn { void Conn(); } [Export("SqlConn", typeof(IDBConn))] public class SqlConn : IDBConn { public void Conn() { Console.WriteLine("this is SqlServer connection"); } }
我们的数据库连接可能会是多种连接,如果我们直接写死为SqlServer的连接,显然不符合开放关闭原则,所以我们提供了一个数据库连接的接口IDBConn,然后用SqlServer的连接SqlConn ,去实现数据库连接接口。
然后利用MEF中的Export将该类型导出至MEF的容器中。
调用:
public string Test() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); //获得容器中的Export IDBConn _conn = IOCContainer.Container.GetExportedValue<IDBConn>("SqlConn"); _conn.Conn(); return ""; }
这是我们的一种方式,当我们的导出到容器中的类过于庞大的时候,我们可以换一种方式来获得导出。
MEF提供Lazy加载,Lazy的好处在于,我们可以先声明,到了需要使用的时候才会实际去加载,代码如下:
public string TestLazy() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); //lazy var export =Container.GetExport<IDBConn>("SqlConn"); IDBConn _conn = export.Value; _conn.Conn(); return ""; }
到此,我们类型说的就差不多了,但是似乎有一点小问题,如果我需要根据某些特定的东西来判断加载哪个类型怎么办?
首先,我们先看下Export的定义:
// // 摘要: // 通过在指定协定名称下导出指定类型,初始化 System.ComponentModel.Composition.ExportAttribute 类的新实例。 // // 参数: // contractName: // 用于导出使用此特性标记的类型或成员的协定名称,或 null 或空字符串 ("") 以使用默认协定名称。 // // contractType: // 要导出的类型。 public ExportAttribute(string contractName, Type contractType);
使用contractName可以满足这个需求,但是MEF提供了更加优雅的方式----元组。
代码如下:
public interface IDBConnMetadata { string Version { get; } } [Export("SqlConn", typeof(IDBConn))] [ExportMetadata("Version", "v1.0.1")] public class SqlConn : IDBConn { public void Conn() { Console.WriteLine("this is SqlServer connection"); } } public string TestMetadata() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); var export = Container.GetExports<IDBConn, IDBConnMetadata>(); //筛选version为v1.0.1的 IDBConn _conn = export.Where(p => p.Metadata.Version.Equals("v1.0.1")).FirstOrDefault().Value; _conn.Conn(); return ""; }
到此类型就完全ok了,接下来看一下方法。
2.方法
方法需要对匿名委托需要有一些了解,不懂的,可以自行查一下,很简单。
public class MySqlConn : IDBConn { private MySqlConn() { } [Export("Conn", typeof(Action))] public void Conn() { Console.WriteLine("this is MySqlConn connection"); } [Export("GetStr", typeof(Func<string>))] private string GetStr() { return "随便试试字符串"; } [Export("GetLength", typeof(Func<string, int>))] public int GetLength(string str) { return str.Length; } }
等一下,上面代码是不是有问题啊?构造方法是私有的,也没有用单例,这怎么调用啊?
首先这又不是静态方法,不能用类名.方法名的方式调用,并且不能实例化,这样岂不是很蒙圈。
那么我们试着用MEF写一下调用,代码如下:
public string TestAction() { TestActionNoPara(); TestActionWithPara(); TestActionWithParaAndReturn(); return ""; } #region Import Action #region Import 无参无返回方法 private void TestActionNoPara() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); var action = Container.GetExport<Action>("Conn"); action.Value.Invoke(); } #endregion #region Import 无参有返回方法 private void TestActionWithPara() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); var action = Container.GetExport<Func<string>>("GetStr"); string message = action.Value.Invoke(); } #endregion #region Import 有参有返回方法 private void TestActionWithParaAndReturn() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); var action = Container.GetExport<Func<string, int>>("GetLength"); int length = action.Value.Invoke("GetLength"); } #endregion #endregion
完全可以访问,并没有因为私有构造方法而造成不能访问,并且上面的GetStr方法也是私有的,依旧可以访问。
这就恶心了,方法的访问修饰符完全被MEF破坏了。。。
感觉这应该算一个bug吧,不过MEF提供了一个属性,可以让方法或者类不能被使用[PartNotDiscoverable]。
3.属性和字段
研究了好久也不知道这个属性和字段应该应用到哪里,有什么实际的应用场景,不过,还是写下这个怎么用。
[Export("SqlConn", typeof(IDBConn))]
public class SqlConn : IDBConn { [Export("Description")] public string Description { get { return "this is SqlServer connection"; } } public void Conn() { Console.WriteLine("this is SqlServer connection"); } }
public string TestAttribute() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); var desc = Container.GetExportedValue<string>("Description"); return desc; } //字段 [Import("SqlConn")] private IDBConn dbConn;
4.构造方法注入
我们也可以用构造方法的方式完成注入,代码如下:
#region 构造注入 [Export("TestB")] public class TestB { private IDBConn _conn; [ImportingConstructor] public TestB([Import("SqlConn")]IDBConn conn) { _conn = conn; } public bool IsInject() { if (_conn == null) { return false; } else { return true; } } } #endregion public string TestInject() { // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //创建容器 var Container = new CompositionContainer(assemblyCatalog); //执行组合 Container.ComposeParts(); var x = Container.GetExportedValue<TestB>("TestB"); bool result = x.IsInject(); return result.ToString(); }
四、改进
上面就是MEF的一些基本使用方法,但是写了这么久发现了似乎有一点问题啊。
每次调用都去创建目录,创建容器,执行组合,似乎太繁琐了,而且重复这些操作,明显也会浪费资源,所以我们可以做一下改进,将这些重复步骤提炼出来一个单独的方法,供其他方法调用。
public class IOCContainer { public static CompositionContainer Container { get; private set; } private static IOCContainer instance = new IOCContainer(); private IOCContainer() { if (Container == null) { //AssemblyCatalog:表示从程序集中搜索部件的目录 // 创建一个程序集目录,用于从一个程序集获取所有的组件定义 var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //指定加载某个程序集 //var directoryCatalog = new DirectoryCatalog("要加载dll的路径"); //AggregateCatalog:聚合目录,可以添加上面所说的所有目录,从而进行多方面的部件搜索 //var aggregateCatalog = new AggregateCatalog(assemblyCatalog, directoryCatalog); //创建容器 Container = new CompositionContainer(assemblyCatalog); //container = new CompositionContainer(aggregateCatalog); //container.GetExportedValue //执行组合 Container.ComposeParts(); } } public static IOCContainer RegisterContainer() { return instance; } }
写好了容器,我们可以在Global.asax中调用RegisterContainer方法,其他使用容器的地方,改为IOCContainer.Container.GetExport就ok了,既简化了代码,也节约了资源。
到这里MEF的总结,就全部完成了,欢迎交流。