【.NET 深呼吸】.net core 中的輕量級 Composition

来源:https://www.cnblogs.com/tcjiaan/archive/2018/09/09/9612593.html
-Advertisement-
Play Games

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • MVVM的特點之一是實現數據同步,即,前臺頁面修改了數據,後臺的數據會同步更新。 上一篇我們已經一起編寫了框架的基礎結構,並且實現了ViewModel反向控制Xaml窗體。 那麼現在就要開始實現數據同步了。 DataContext—數據上下文 在實現數據同步前,我們要瞭解一個知識點——DataCon ...
  • 跨語言調用Hangfire定時作業服務 背景 Hangfire允許您以非常簡單但可靠的方式執行後臺定時任務的工作。內置對任務的可視化操作。非常方便。 但令人遺憾的是普遍都是業務代碼和hagnfire服務本身聚合在一個程式中運行,極大的限制了hangfire的擴展和跨語言調用。 所以萌生了開發一個支持 ...
  • 1. .NET和C#有什麼區別 答:.NET一般指 .NET FrameWork框架,它是一種平臺,一種技術。 C#是一種編程語言,可以基於.NET平臺的應用。 2.一列數的規則如下: 1、1、2、3、5、8、13、21、34...... 求第30位數是多少,用遞歸演算法實現。答:public cla ...
  • 類的定義 類是描述具有相同特征與行為的事物的抽象,類內部包含類的特征和類的行為 類支持繼承 類的定義是關鍵字class為標誌 類的格式 訪問標識符 class 類名 { 類主體 } 訪問標識符:指定了類及其成員的訪問規則。如果不指定則使用預設的標識符 類的預設標識符為internal,而類成員的預設 ...
  • 概述 在上面一篇 Windows Community Toolkit 4.0 - DataGrid - Part02 中,我們針對 DataGrid 控制項的 Utilities 部分做了詳細分享。而在本篇,我們會對控制項中最重要的 DataGrid 文件夾中的類做詳細的分享。 下麵是 Windows ...
  • 參數 var list = new List<int>(); // 集合var totalCount = 17; // 總數量var pageSize = 5; // 每頁查詢數量 第一種: var pageTotal = totalCount % pageSize == 0 ? totalCoun ...
  • 之前的博客 將時間作為GUID的方法 中,我使用了鎖。我在實際的使用中,錯將鎖的釋放放在了if語句中,這純粹是我的失誤,導致了很嚴重的錯誤。因此我在想是否有無鎖的將時間作為GUID的方式,答案是使用Interlocked中的 CompareExchange方法,該方法是原子操作。說是無鎖操作,其實就 ...
  • 方法是什麼 方法是C#中將一堆代碼進行進行重用的機制 他是在類中實現一種特定功能的代碼塊,將重覆性功能提取出來定義一個新的方法 這樣可以提高代碼的復用性,使編寫程式更加快捷迅速 方法格式 訪問修飾符 返回類型 方法名稱(參數列表) { 方法體; } 方法是在類或結構中聲明的,聲明時需要訪問修飾符、返 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...