記得前面老周寫過在.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
雖然在官方 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,今天老周就介紹這麼多,內容也應該覆蓋得差很少了。肚子餓了,準備開飯。