【.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,今天老周就介紹這麼多,內容也應該覆蓋得差很少了。肚子餓了,準備開飯。 

相關文章
相關標籤/搜索