【.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,下面我们说干货。spa

一、安装须要的 NuGet 包

虽然在官方 docs 上,.net core API 目录收录了 System.Composition ,但默认安装的 .net core 库中是不包含 System.Composition 的,须要经过 Nuget 来安装。在 Nuget 上搜索 System.Composition,你会看到有好几个库。.net

那到底要安装哪一个呢?很简单,选名字最短那个,其余几个由于存在依赖关系,会自动安装的。debug

这里老周介绍用命令来安装,很方便。在 VS 主窗体中,打开菜单【工具】-【NuGet 包管理器】-【程序包管理器控制台】,这样你就打开了一个命令窗口,而后输入:code

 Install-Package System.Composition

须要说的,输入的内容是不区分大小写的,你能够所有输入小写。这风格是很 PowerShell 的,这个很好记,PS 风格的命令都是“动词 + 名词”,中间一“减号”,好比,Get-Help。因此,安装的单词是 Install,程序包是 Package,安装包就是 Install-Package,而后你能够猜一下,那么卸载 Nuget 包呢,Uninstall-Package,那更新呢,Update-Package,查找包呢,Find-Package……对象

你要是不信,能够执行一下 get-help Nuget 看看。blog

好了,执行完对 System.Composition 的安装,它会自动把依赖的库也安装。

不带其余参数的 install-package ,默认会安装最新版本的库,因此说,执行这个来安装很方便。

 

二、导出类型

类型的导出方法与标准的 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 => "人渣";
    }

 

三、构建容器

在组装扩展时,须要一个容器,用来导入或收集这些组件,以供代码调用。在“轻量级”版本中,容器的用法与标准的 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 做为约束)列表,而后就能够逐个去调用了。结果以下图所示。

 

四、导入类型

导入的时候,除了调用 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();

 

五、导出元数据

元数据不是类型的一部分,但能够做为类型的附加信息。有些时候是须要的,尤为是在实际使用时,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,今天老周就介绍这么多,内容也应该覆盖得差很少了。肚子饿了,准备开饭。 

相关文章
相关标签/搜索