首頁 > 軟體

C#在MEF框架中實現延遲載入部件

2022-06-23 14:00:42

在MEF的宿主中,當我們通過Import宣告匯入的物件時,組裝(Compose)的時候會建立該物件。例如:

    interface ILogger
    {
        void Log(string message);
    }

    [Export(typeof(ILogger))]
    class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine("logger 1" + message);
        }
    }

    class Host
    {
        [Import]
        ILogger _logger = null;

        public Host()
        {
            var catalog = new AssemblyCatalog(this.GetType().Assembly);
            var container = new CompositionContainer(catalog);

            //這兒會建立ConsoleLogger物件
            container.ComposeParts(this);

            _logger.Log("hello world");
        }
    }

有的時候,有些元件的建立開銷比較大,但又不會立即使用。此時,我們希望通過延遲初始化的方式將其延遲到使用的時候建立,從而提高效能(常見的是提高啟動速度)。MEF是支援這一模式的,我們只需要修改一下匯入的宣告形式即可。

    [Import]
    Lazy<ILogger> _logger = null;

這樣,Logger就會延遲到第一次使用的時候建立了。

後設資料MetaData

有的時候,對於同一個服務有多個提供者,我們需要從中選擇一個使用。MEF提供了ImportMany來解決這一需求。

有的時候,對於同一個服務有多個提供者,我們需要從中選擇一個使用。MEF提供了ImportMany來解決這一需求。

    [Export(typeof(ILogger))]
    class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

    [Export(typeof(ILogger))]
    class DbLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

    class Host
    {
        [ImportMany]
        ILogger[] _logger = null;


        public Host()
        {
            var catalog = new AssemblyCatalog(this.GetType().Assembly);
            var container = new CompositionContainer(catalog);

            container.ComposeParts(this);

            _logger.FirstOrDefault(i => i is DbLogger).Log("hello world");
        }
    }

此時,如果我們想使用延遲匯入的時候,就會變成如下形式:

    class Host
    {
        [ImportMany]
        Lazy<ILogger>[] _loggerServices = null;

        public Host()
        {
            var catalog = new AssemblyCatalog(this.GetType().Assembly);
            var container = new CompositionContainer(catalog);

            //這兒會建立ConsoleLogger物件
            container.ComposeParts(this);

            _loggerServices.FirstOrDefault(i => i.Value is DbLogger).Value.Log("hello world");
        }
    }

咋一看並沒有什麼問題,所有的Logger都是延遲建立的。但是仔細分析一下就會發現,要找到DbLogger的時候,必須遍歷所有的_loggerServices,這個遍歷會導致建立Logger。也就是說,使用第一個Logger的時候可能建立所有的Logger。

那麼,如何實現我們只建立所需要的Logger呢? 這個時候就輪到後設資料出場了,MEF中的後設資料可以將一個資料附加到Export的服務物件中一併匯出,從而可以通過元素據找到對應的服務。首先我們看看最終的效果吧:

    public interface ILoggerData
    {
        string Name { get; }
    }

    class Host
    {
        [ImportMany]
        Lazy<ILogger, ILoggerData>[] _logger = null;

        public Host()
        {
            var catalog = new AssemblyCatalog(this.GetType().Assembly);
            var container = new CompositionContainer(catalog);

            container.ComposeParts(this);

            _logger.FirstOrDefault(i => i.Metadata.Name == "DB Logger").Value.Log("hello world");
        }
    }

這裡首先宣告了一個後設資料型別的介面ILoggerData,然後,匯入的物件變成了Lazy<ILogger, ILoggerData>,這個物件有一個屬性為Metadata,它的型別就是剛才宣告的ILoggerData。匯出的ILogger物件是延遲建立的,而後設資料不是延遲建立的。我們可以通過遍歷ILoggerData來找到所需要的Logger物件,從而實現只建立所使用的Logger物件。

現在的問題是:如何在匯出的時候宣告相關的後設資料。MEF提供了兩種方式:

通過ExportMetadataAttribute標記宣告

這種方式是在匯出的服務的時候一併通過ExportMetaDataAttribute屬性標記元素據:

    [ExportMetadata("Name", "Console Logger")]
    [Export(typeof(ILogger))]
    class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

    [ExportMetadata("Name", "DB Logger")]
    [Export(typeof(ILogger))]
    class DbLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

ExportMetaDataAttribute有兩個引數,Name和Value,Name為屬性名稱,Value為屬性值。Compse的時候,MEF會先建立一個實現了後設資料物件,然後將根據將Value值賦值給Name所對應的屬性名稱。

這麼做雖然比較簡單,但是它有兩個弊端:

  • 1. 屬性名稱不是強型別

這裡我們必須字串來給標誌屬性名稱,它們之間並沒有語法級的一致性檢查,在缺少nameof運運算元的現在,一旦後設資料屬性名稱更改的話是非常容易出錯的。

  • 2. 如果後設資料有多個值的話賦值顯得非常累贅。

假如我們增加了一個Priority型別的屬性,

    public interface ILoggerData
    {
        string Name { get; }
        int Priority { get; }
    }

此時,必須對所有的匯出物件都增加一個ExportMetadata標記,寫出如下形式:

    [ExportMetadata("Name", "DB Logger")]
    [ExportMetadata("Priority", 3)]

當屬性更多一點的話相信程式設計師們就會罵娘了。並且一旦某個Export物件漏寫了,改物件就不會被匯入。這個是一個執行時的錯誤,非常不容易排查的。

通過Attribute標記

這種方式可以通過一個Attribute來標記後設資料:

    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    class LoggerDataAttribute : Attribute, ILoggerData
    {
        public string Name { get; private set; }

        public LoggerDataAttribute(string name)
        {
            this.Name = name;
        }
    }

    [LoggerData("Console Logger")]
    [Export(typeof(ILogger))]
    class ConsoleLogger : ILogger, ILoggerData
    {
        public string Name { get; set; }

        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

    [LoggerData("DB Logger")]
    [Export(typeof(ILogger))]
    class DbLogger : ILogger, ILoggerData
    {
        public string Name { get; set; }
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

首先,宣告一個LoggerDataAttribute,這個Attribute必須被MetadataAttribute標記。然後,在Export的物件前加上該LoggerDataAttribute,這樣MEF匯入的時候就會根據該LoggerDataAttribute建立後設資料了。

值得一提的是,這裡的LoggerDataAttribute本身並不需要實現ILoggerData介面,它是一個DuckType的約定,只需要實現後設資料的屬性即可。我這裡實現該介面主要是為了讓編譯器保障後設資料屬性都有被準確實現。

到此這篇關於C#在MEF框架中實現延遲載入部件的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com