记得前面老周写过在.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,今天老周就介绍这么多,内容也应该覆盖得差不多了。肚子饿了,准备开饭。

【.NET 深呼吸】.net core 中的轻量级 Composition的更多相关文章

  1. 如何在 ASP.NET Core 中构建轻量级服务

    在 ASP.NET Core 中处理 Web 应用程序时,我们可能经常希望构建轻量级服务,也就是没有模板或控制器类的服务. 轻量级服务可以降低资源消耗,而且能够提高性能.我们可以在 Startup 或 ...

  2. 在.NET Core中使用MEF

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:微软的可托管扩展框架也移植到.NET Core上了. 可托管扩展框架(Managed ...

  3. ASP.NET Core 中文文档 第四章 MVC(01)ASP.NET Core MVC 概览

    原文:Overview of ASP.NET Core MVC 作者:Steve Smith 翻译:张海龙(jiechen) 校对:高嵩 ASP.NET Core MVC 是使用模型-视图-控制器(M ...

  4. ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器)

    前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ...

  5. ASP.NET Core中使用IOC三部曲(二.采用Autofac来替换IOC容器,并实现属性注入)

    前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ...

  6. 在 .NET Core 中使用 DiagnosticSource 记录跟踪信息

    前言 最新一直在忙着项目上的事情,很久没有写博客了,在这里对关注我的粉丝们说声抱歉,后面我可能更多的分享我们在微服务落地的过程中的一些经验.那么今天给大家讲一下在 .NET Core 2 中引入的全新 ...

  7. Asp.Net Core中服务的生命周期选项区别和用法

    在做一个小的Demo中,在一个界面上两次调用视图组件,并且在视图组件中都调用了数据库查询,结果发现,一直报错,将两个视图组件的调用分离,单独进行,却又是正常的,寻找一番,发现是配置依赖注入服务时,对于 ...

  8. ASP.NET Core 中的 ORM 之 Entity Framework

    目录 EF Core 简介 使用 EF Core(Code First) EF Core 中的一些常用知识点 实体建模 实体关系 种子数据 并发管理 执行 SQL 语句和存储过程 延迟加载和预先加载 ...

  9. TransactionScope事务处理方法介绍及.NET Core中的注意事项 SQL Server数据库漏洞评估了解一下 预热ASP.NET MVC 的VIEW [AUTOMAPPER]反射自动注册AUTOMAPPER PROFILE

    TransactionScope事务处理方法介绍及.NET Core中的注意事项   作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/10170712.ht ...

随机推荐

  1. union和union all的区别(面试常考)

    Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序: Union All:对两个结果集进行并集操作,包括重复行,不进行排序: Union因为要进行重复值扫描,所以效率低.如果合 ...

  2. 利用图片的灰度平均值来进行分类实现手写图片识别(数据集50000张图片)——Jason niu

    from collections import defaultdict import mnist_loader def main(): training_data, validation_data, ...

  3. aar上传maven库工具

    需求:本地aar文件上传到maven库 参考我之前的博客gradle上传本地文件到远程maven库(nexus服务器) 下面是java图形化工具代码 package com.jinkejoy.buil ...

  4. python之psutil模块(获取系统性能信息(CPU,内存,磁盘,网络)

    一.psutil模块 1. psutil是一个跨平台库(http://code.google.com/p/psutil/),能够轻松实现获取系统运行的进程和系统利用率(包括CPU.内存.磁盘.网络等) ...

  5. springboot2 redis

    本项目使用jar包: <parent> <groupId>org.springframework.boot</groupId> <artifactId> ...

  6. Mac终端下的svn使用教程

    1.将文件checkout到本地目录 1 svn checkout path(path是服务器上的目录) 2 例如:svn checkout svn://192.168.1.1/svn 3 简写:sv ...

  7. C#为什么要多线程开发(一)

    首先说下多线程出现的原因: 为了解决负载均衡问题,充分利用CPU资源.为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.为了处理大量的IO操作时或处理的情况需要花费大量的时间等等 ...

  8. 关于Git安装和操作中可能碰到的问题

    markdown PDF 大致的安装流程和操作方法可以参照学长给的 Git和GitHub的简单教程 但是在具体实践过程中可能会碰到一些问题 下载 SSH key 先有远程库,要克隆一个本地库 先有本地 ...

  9. BZOJ.2006.[NOI2010]超级钢琴(贪心 堆)

    BZOJ 洛谷 思路和BZOJ3784一样,用前缀和+堆维护.做那题吧,不赘述啦. (没错我就是水一个AC) //54620kb 1060ms #include <queue> #incl ...

  10. Python发送QQ邮件

    #!/usr/bin/python # -*- coding: UTF-8 -*-    import smtplib from email.mime.text import MIMEText fro ...