記得前面老周寫過在.net core 中使用 Composition 的爛文。上回老周給大伙伴們介紹的是一個“重量級”版本—— System.ComponentModel.Composition。應該說,這個“重量級”版本是.NET 框架中的“標配”。 很多東西都會有雙面性,MEF 也一樣,對於擴展 ...
記得前面老周寫過在.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("[email protected]");
最後調用的結果如下
IMAP:來自[email protected]的郵件
當然了,元數據還有更高級的玩法,你要是覺得附加 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,今天老周就介紹這麼多,內容也應該覆蓋得差不多了。肚子餓了,準備開飯。