【.NET 深呼吸】.net core 中的轻量级 Composition
记得前面老周写过在.net core 中使用 composition 的烂文。上回老周给大伙伴们介绍的是一个“重量级”版本—— system.componentmodel.composition。应该说,这个“重量级”版本是.net 框架中的“标配”。
很多东西都会有双面性,mef 也一样,对于扩展组件灵活方便,同时也带来性能上的一些损伤。但这个损伤应只限于应用程序初始化阶段,一般来说,我们也不需要频繁地去组合扩展,程序初始化时执行一次就够了。所以,性能的影响应在开始运行的时候。
与“重量级”版本在同一天发布的,还有一个“轻量级”版本—— system.composition。相对于“标配”,这个库简洁了许多,与标准 mef 相比,使用方法差不多,只是有细微的不同,这个老周稍后会讲述的,各位莫急。还有一个叫 microsoft.composition 的库,这个是旧版本的,适用于 windows 8/8.1 的应用。对于 core,可以不考虑这个版本。
system.composition 相对于标准的 mef,是少了一些功能的,尤其是对组件的搜索途径,mef 的常规搜索途径有:应用程序范围、程序集范围、目录(文件夹)范围等。而“轻量级”版本只在程序集范围中搜索。这也很适合.net core 程序,尤其是 web 项目。
好了,以上内容皆是纸上谈 b,下面咱们说干货。
1、安装需要的 nuget 包
虽然在官方 docs 上,.net core api 目录收录了 system.composition ,但默认安装的 .net core 库中是不包含 system.composition 的,需要通过 nuget 来安装。在 nuget 上搜索 system.composition,你会看到有好几个库。
那到底要安装哪个呢?很简单,选名字最短那个,其他几个因为存在依赖关系,会自动安装的。
这里老周介绍用命令来安装,很方便。在 vs 主窗体中,打开菜单【工具】-【nuget 包管理器】-【程序包管理器控制台】,这样你就打开了一个命令窗口,然后输入:
install-package system.composition
需要说的,输入的内容是不区分大小写的,你可以全部输入小写。这风格是很 powershell 的,这个很好记,ps 风格的命令都是“动词 + 名词”,中间一“减号”,比如,get-help。所以,安装的单词是 install,程序包是 package,安装包就是 install-package,然后你可以猜一下,那么卸载 nuget 包呢,uninstall-package,那更新呢,update-package,查找包呢,find-package……
你要是不信,可以执行一下 get-help nuget 看看。
好了,执行完对 system.composition 的安装,它会自动把依赖的库也安装。
不带其他参数的 install-package ,默认会安装最新版本的库,所以说,执行这个来安装很方便。
2、导出类型
类型的导出方法与标准的 mef 一样的,比如这样。
[export] public class flydisk { }
于是,这个 flydisk 类就被导出了。你也可以为导出设置一个协定名,在合并组件后方便挑选。
[export("fly")] public class flydisk { }
当然了,如果你的组件扩展模式是 接口 + 实现,通常为了兼容和规范,应该有个接口。这时候你标注 export 特性时,要指明协定的 type。
[export(typeof(iperson))] public class bailei : iperson { public string name => "败类"; }
如果你希望更严格地约束导入和导出协定,还可以同时指定 name 和 type。
[export("rz", typeof(iperson))] public class renzha : iperson { public string name => "人渣"; }
3、构建容器
在组装扩展时,需要一个容器,用来导入或收集这些组件,以供代码调用。在“轻量级”版本中,容器的用法与标准的 mef 区别较大,mef 中用的是 compositioncontainer 类,但在 system.composition 中,我们需要先创建一个 containerconfiguration,然后再创建容器。容器由 compositionhost 类表示。
来,看个完整的例子。首先是导出类型。
public interface iperson { string name { get; } void work(); } [export(typeof(iperson))] public class bailei : iperson { public string name => "败类"; public void work() { console.writeline("影响市容。"); } }
然后,创建 containerconfiguration。
containerconfiguration config = new containerconfiguration().withassembly(assembly.getexecutingassembly());
containerconfiguration 类的方法,调用风格也很像 asp.net core,withxxx 方法会把自身实例返回,以方便连续调用。上面代码是设置查找扩展组件的程序集,这里我设定为当前程序集,如果是其他程序集,可以用 load 或 loadfrom 方法先加载程序集,然后再调用 withassembly 方法,原理差不多。
随后,便可以创建容器了。
using(compositionhost host = config.createcontainer()) { }
调用 getexport 方法可以直接获取到导出类型的实例。
using(compositionhost host = config.createcontainer()) { iperson p = host.getexport<iperson>(); console.write($"{p.name},"); p.work(); }
那,如果某个协定接口有多个实现类导出呢。咱们再看一例。
首先,定义公共的协定接口。
public interface icd { void play(); }
再定义两个导出类,都实现上面定义的接口。
[export(typeof(icd))] public class dbcd : icd { public void play() { console.writeline("正在播放盗版 cd ……"); } } [export(typeof(icd))] public class blcd : icd { public void play() { console.writeline("正在播放蓝光 cd ……"); } }
然后,跟前一个例子一样,创建 containerconfiguration 实例,再创建容器。
assembly curassembly = assembly.getexecutingassembly(); containerconfiguration cfg = new containerconfiguration(); cfg.withassembly(curassembly); using(compositionhost host = cfg.createcontainer()) { …… }
接下来就是区别了,因为实现 icd 接口并且标记为导出的类有两个,所以要调用 getexports 方法。
using(compositionhost host = cfg.createcontainer()) { ienumerable<icd> cds = host.getexports<icd>(); foreach (icd c in cds) c.play(); }
返回来的是一个 icd (实际是 icd 的实现类,但以 icd 作为约束)列表,然后就可以逐个去调用了。结果如下图所示。
4、导入类型
导入的时候,除了调用 getexport 方法外,还可以定义一个类,然后把类中的某个属性标记为由导入的类型填充。
看例子。先上接口。
public interface ianimal { void eating(); }
然后上实现类,并标为导出类型。
[export(typeof(ianimal))] public class dog : ianimal { public void eating() { console.writeline("狗吃 shi"); } }
定义一个类,它有一个 mypet 属性,这个属性由 composition 来导入类型实例,并赋给它。
public class peoplelovepets { [import] public ianimal mypet { get; set; } }
注意有一点很重要,mypet 属性上一定要加上 import 特性,因为 composition 在组装类型时会检测是否存在 import 特性,如果你不加的话,扩展组件就不会导入到 mypet 属性上的。
接着,创建容器的方法与前面一样。
containerconfiguration cfg = new containerconfiguration() .withassembly(assembly.getexecutingassembly()); peoplelovepets pvl = new peoplelovepets(); using(var host = cfg.createcontainer()) { host.satisfyimports(pvl); }
但你会看到有差别的,这一次,要先创建 peoplelovepets 实例,后面要调用 satisfyimports 方法,在 peoplelovepets 实例上组合导入的类型。
最后,你通过 mypet 属性就能访问导入的对象了,以 ianimal 为规范,实际类型是 dog。
ianimal an = pvl.mypet; an.eating();
那,如果导出的类型是多个呢,这时就不能只用 import 特性了,要用 importmany 特性,而且接收导入的 mypet 属性要改为 ienumerable<ianimal>,表示多个实例。
public class peoplelovepets { [importmany] public ienumerable<ianimal> mypet { get; set; } }
为了应对这种情形,我们再添加一个导出类型。
[export(typeof(ianimal))] public class cat : ianimal { public void eating() { console.writeline("猫吃兔粮"); } }
创建容器和执行导入的处理过程都不变,但访问 mypet属性的方法要改了,因为现在它引用的不是单个实例了。
foreach (ianimal an in pvl.mypet) an.eating();
5、导出元数据
元数据不是类型的一部分,但可以作为类型的附加信息。有些时候是需要的,尤其是在实际使用时,composition 组合它所找到的各种扩展组件,但在调用时,可能不会全部都调用,需要筛选出需要调用的那部分。
为导出类型添加元数据有两种方法。先说第一种,很简单,直接在导出类型上应用 exportmetadata 特性,然后设置 name 和 value,每个 exportmetadataattribute 实例就是一条元数据,你会发现,它其实很像 key / value 结构。
看个例子,假设有这样一个公共接口。
public interface imail { void readbody(string from); }
然后有两个导出类型。
[export(typeof(imail))] public class mailloader1 : imail { public void readbody(string from) { console.writeline($"pop3:来自{from}的邮件"); } } [export(typeof(imail))] public class mailloader2 : imail { public void readbody(string from) { console.writeline($"imap:来自{from}的邮件"); } }
这两种类型所处理的逻辑是不同的,第一个是通过 pop3 收到的邮件,第二个是通过 imap 收到的邮件。为了在导入类型后能够进行判断和区分,可以为它们分别附加元数据。
[export(typeof(imail))] [exportmetadata("prot", "pop3")] public class mailloader1 : imail { …… } [export(typeof(imail))] [exportmetadata("prot", "imap")] public class mailloader2 : imail { …… }
在导入带元数据的类型时,可以用到这个类——lazy<t, tmetadata>,它是 lazy<t> 的子类,类如其名,就是延迟初始化的意思。
定义一个 mailreader 类,公开一个 loaders 属性。
public class mailreader { [importmany] public ienumerable<lazy<imail, idictionary<string, object>>> loaders { get; set; } }
注意这里,lazy 的 tmetadata,默认的实现,通过 idictionary<string, object> 是可以存储导入的元数据的。上面咱们也看到,元数据在导出时,是以 name / value 的方式指定的,相当类似于字典的结构,所以,用字典数据类型自然就能存放导入的元数据。
执行导入的代码就很简单了,跟前面的例子差不多。
containerconfiguration cfg = new containerconfiguration() .withassembly(assembly.getexecutingassembly()); mailreader mlreader = new mailreader(); using(compositionhost host = cfg.createcontainer()) { host.satisfyimports(mlreader); }
这时候,我们在访问导入的类型时,就可以根据元数据进行筛选了。
在这个例子中,咱们只调用带 imap 的邮件阅读器。
imail m = (from o in mlreader.loaders let t = o.metadata["prot"] as string where t == "imap" select o).first().value; m.readbody("da_sb@ppav.com");
最后调用的结果如下
imap:来自da_sb@ppav.com的邮件
当然了,元数据还有更高级的玩法,你要是觉得附加 n 条 exportmetadata 特性太麻烦,你还可以自己定义一个类来包装,注意在这个类上要标记 metadataattribute 特性,而且从 attribute 类派生。为啥呢?因为元数据是不参与类型逻辑的,你要把它附加到类型上,只能作为 特性 来处理。
[attributeusage(attributetargets.class)] [metadataattribute] public class extmetadatainfoattribute : attribute { public string remarks { get; set; } public string author { get; set; } public string publishtime { get; set; } }
之后,就可以直接应用到导出类型上面了。
public interface itest { void runtask(); } [export(typeof(itest))] [extmetadatainfo(author = "单眼明", publishtime = "2018-9-18", remarks = "已 debug 了 71125 次")] public class democomp : itest { public void runtask() { console.writeline("demo 组件被调用"); } } [export(typeof(itest))] [extmetadatainfo(author = "大神威", publishtime = "2018-10-5", remarks = "预览版")] public class plaincomp : itest { public void runtask() { console.writeline("plain 组件被调用"); } }
导入时,同样可以 import 到一个属性中。
public class myapppool { [importmany] public ienumerable<lazy<itest, idictionary<string, object>>> components { get; set; } }
创建容器的方法一样。
containerconfiguration cfg = new containerconfiguration() .withassembly(assembly.getexecutingassembly()); myapppool pool = new myapppool(); using(var host = cfg.createcontainer()) { host.satisfyimports(pool); }
尝试枚举出导入类型的元数据。
foreach (var ext in pool.components) { var metadata = ext.metadata; console.writeline($"{ext.value.gettype()} 的元数据:"); foreach (var kv in metadata) { console.writeline($"{kv.key}: {kv.value}"); } console.writeline(); }
执行结果如下图。
要是你觉得用 idictionary<string, object> 类型来存放导入的元数据也很麻烦,那你也照样可以定义一个类来存放,但这个类要符合两点:a、带有无参数的公共构造函数,因为它是由 composition 内部来实例化的;b、属性必须是公共并且有 get 和 set 访问器,即可写的,不然没法设置值了,而且属性名必须与导出时的元数据名称相同。
现在我们改一下刚刚的例子,定义一个类来存放导入的元数据。
public class importedmetadata { public string author { get; set; } public string remarks { get; set; } public string publishtime { get; set; } }
然后,myapppool 类也可以改一下。
public class myapppool { //[importmany] //public ienumerable<lazy<itest, idictionary<string, object>>> components { get; set; } [importmany] public ienumerable<lazy<itest, importedmetadata>> components { get; set; } }
最后,枚举元数据的代码也改一下。
foreach (var ext in pool.components) { var metadata = ext.metadata; console.writeline($"{ext.value.gettype()} 的元数据:"); console.writeline($"author: {metadata.author}\nremarks: {metadata.remarks}\npublishtime: {metadata.publishtime}"); console.writeline(); }
====================================================================
好了,关于 system.composition,今天老周就介绍这么多,内容也应该覆盖得差不多了。肚子饿了,准备开饭。
上一篇: PS制作水晶球之夜的电脑桌面
推荐阅读
-
ASP.NET Core 中的管道机制
-
ASP.NET Core 2.1中基于角色的授权
-
ASP.NET Core应用中与第三方IoC/DI框架的整合
-
ASP.NET Core 中的模型绑定操作详解
-
ASP.NET Core 2.1 中的 HttpClientFactory (Part 2) 定义命名化和类型化的客户端
-
使用VS2022在ASP.NET Core中构建轻量级服务
-
谈谈C#中的特性:在.net core中的应用
-
谈谈ASP.NET Core MVC设计中的Controller与Action设计规范
-
【5min+】 一个令牌走天下!.Net Core中的ChangeToken
-
微服务统计,分析,图表,监控一体化的HttpReports项目在.Net Core 中的使用