.Net Core中的配置Configuration使用以及源碼解析


在以前的.Net Framework程序中,我們的很多配置都會寫到App.config或者Web.config,然后通過系統提供的System.Configuration.ConfigurationManager去獲取相應的配置,但是在.Net Core 我們有了新的配置獲取方式,並且不只是支持config文件,默認實現了ini,xml,json等一系列文件類型的獲取方式,並且他們的獲取方式是統一的,它做到了不同的配置源,統一的獲取方式。

使用IConfiguration來獲取配置信息

新建一個Asp.Net Core MVC 應用,這里你也可以建控制台應用,只是網站應用方便演示,兩者沒有區別。唯一不同的地方是網站已經為我們創建了ConfigurationBuilder對象,並且注入到了容器中,同時也添加了一些默認的配置。

在項目里添加一個config01.json文件,內容如下:

{
  "EnableCache": true,
  "Email": {
    "From": "from@email.com",
    "To": "to@email.com"
  },
  "Type": {
    "Assembly": {
      "Namespace": {
        "FullName": "CoreConfiguration.Controllers.HomeController"
      }
    }
  }
}

接着在Programm.cs文件中找到CreateWebHostBuilder方法,修改如下:

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, configBuilder) =>
            {     
                configBuilder.AddJsonFile("config01.json");
            })
            .UseStartup<Startup>();
    

然后可以在任意一個地方通過依賴注入的方式獲取到IConfiguration這個對象,通過這個對象就可以獲取對應的配置,例如:

    public class HomeController : Controller
    {
        IConfiguration _configuration;
        public HomeController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public IActionResult Start()
        {
            ViewBag.Item1 = _configuration["EnableCache"];
            ViewBag.Item2 = _configuration["Email"];

            IConfigurationSection emailConfiguration = _configuration.GetSection("Email");
            ViewBag.Item3 = emailConfiguration["To"];
            ViewBag.Item4 = emailConfiguration["From"];

            ViewBag.Item5 = _configuration["Email:To"];// why is ":" ?
            ViewBag.Item6 = _configuration["Email:From"];

            IConfigurationSection emailToConfiguration = emailConfiguration.GetSection("To");
            ViewBag.Item7 = emailToConfiguration.Value;//Same with emailConfiguration["To"]

            ViewBag.Item8 = _configuration.GetSection("Type").GetSection("Assembly").GetSection("Namespace")["FullName"];

            return View();
        }
}

最終得到的結果如下:

在這個使用的例子中,我們可以看到,對於簡單的值可以直接以鍵值對的方式獲取,而復雜的值,則會返回null,這似乎不太合理,但這是和它解析文件后存儲內容的方式有關,后續會詳細解釋。

如果我們想要獲取這種層級的值有兩種方式:1.先通過GetSection方法獲取對應的子節點,再去獲取。2.直接在傳遞鍵(key)的時候,進行組合並且以分號(:)隔開,這樣也是可以直接獲得的。

另外我們可以看到在調用GetSection返回的是IConfigurationSection對象,而不是IConfiguration,實際上IConfigurationSection是繼承於IConfiguration,所以我們一樣可以在IConfigurationSection直接獲取值。另外我們在HomeController里注入進來的那個配置對象,其實是一個IConfigurationRoot,當然它也是繼承於IConfiguration,所以他們三者之間UML圖,如下:

對於定義在IConfiguration里的幾個成員,自不必說,大家都能理解。而在IConfigurationSection中,Path代表了當前子節點路徑,Key代表了當前子節點的名稱,而value就是當前子節點代表的值,如下兩個例子,會讓大家更容易理解幾個成員的意思:

var section1 = _configuration.GetSection("Type").GetSection("Assembly").GetSection("Namespace");

section1:

  • Key:  Namespace
  • Path: Type:Assembly:Namespace
  • Value: null
var section2 = _configuration.GetSection("Type").GetSection("Assembly").GetSection("Namespace").GetSection("FullName");

section2:

  • Key: FullName
  • Path: Type:Assembly:Namespace:FullName
  • Value: CoreConfiguration.Controllers.HomeController

而IConfigurationRoot里,只多了一個IConfigurationProvider的枚舉,而這個IConfigurationProvider對象,就是實際存儲數據的地方,后續會詳細解釋。

講到這里,大家對配置的獲取,應該有了一個基本的認識。但是應該會有一個疑問,比如在例子中,我們能不能直接獲取整個Email相關的配置並且映射成一個類對象,而不是像上面一樣一個一個的獲取?毫無疑問,肯定是可以的。

 

使用Binder獲取整個節點的配置

使用方法可以直接看如下代碼:

        public IActionResult Binder()
        {
            IConfigurationSection emailConfiguration = _configuration.GetSection("Email");
            var emailOption = emailConfiguration.Get<EmailOption>();
            ViewBag.Item1 = emailOption.From;
            ViewBag.Item2 = emailOption.To;

            //string str1 = _configuration["EnableCache"];
            //bool enableCache = bool.Parse(str1);
            bool enableCache = _configuration.GetSection("EnableCache").Get<bool>();
            ViewBag.Item3 = enableCache;

            return View();
        }

    public class EmailOption
    {
        public string From { get; set; }

        public string To { get; set; }
    }

最后輸出得到的值和第一個例子別無二致:

在例子里我們調用了擴展方法:public static T Get<T>(this IConfiguration configuration);使用它,必須添加Microsoft.Extensions.Configuration.Binder這個nuget包,如果是網站程序,這個包默認已經引入進來。可以看到,這個方法是基於IConfiguration的,所以IConfigurationSection和IConfigurationRoot都可以使用它。

 

多數據源的支持

在我們的項目當中,可能有不止一個配置文件,甚至有不同的類型的配置文件,那么我們只要依次添加這些配置就行了。假如我們的項目中還有如下兩個配置文件:

config02.ini

LogType=Log4Net

[Email]
Body=BodyIni
Subject=Subject

config03.xml

<?xml version="1.0" encoding="utf-8" ?>
<config>
  <RetryCount>10</RetryCount>
  <Email>
    <Body>BodyXml</Body>
    <BodyType>Html</BodyType>
  </Email>
</config>

同時我們也需要一些基於內存數據的配置,那么在Programm.cs修改如下代碼:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
            {

                  config.AddInMemoryCollection(new Dictionary<string, string>() {
                        { "LogLevel","Debug"},
                        { "Email:UseSSL","true"}
                  });

                  config.AddJsonFile("config01.json");

                config.AddIniFile("config02.ini");
                config.AddXmlFile("config03.xml");

            })
            .UseStartup<Startup>();
    }

添加如下方法:

        public IActionResult MultiSource()
        {
            IConfigurationSection emailConfiguration = _configuration.GetSection("Email");
            MultiEmailOption multiEmailOption = emailConfiguration.Get<MultiEmailOption>();

            //InMemory
            ViewBag.Item1 = _configuration["LogLevel"];
            ViewBag.Item2 = emailConfiguration["UseSSL"];

            //Xml
            ViewBag.Item3 = _configuration["RetryCount"];
            ViewBag.Item4 = emailConfiguration["BodyType"];

            //Ini
            ViewBag.Item5 = _configuration["LogType"];
            ViewBag.Item6 = emailConfiguration["Subject"];

            ViewBag.Item7 = Newtonsoft.Json.JsonConvert.SerializeObject(multiEmailOption);
            return View();
        }

    public class MultiEmailOption : EmailOption
    {
        public string Body { get; set; }

        public bool UseSSL { get; set; }

        public string Subject { get; set; }

        public string BodyType { get; set; }
    }

最后輸出的結果如下:

 

我們可以看到來源於不同的Source的配置,最終可以被整合成到同一個對象,這得益於所有不同的文件最終都會被解析成相同結構的數據源。

另外,最終取到的Email.Body的值為BodyXml,雖然在ini文件中也定義了該屬性的值,但是框架會按添加數據源的逆序依次匹配數據源里的數據。利用這一特性可以實現不同環境的不同配置。

可監控的數據源

以前的.Net Framework程序,不論是桌面還是網站,修改配置源以后都必須重啟程序才能生效。但是在.Net Core中,在添加參數時,指定reloadOnChange參數就可以在不重啟應用的情況下獲取新的配置,但是啟用這個特性有性能損耗,如非必要,不用添加此參數以啟用監控。對Json類型的配置文件,在添加時,如此調用就可以啟用監控:

config.AddJsonFile("config04.json",true,true);

四大配置對象

在上面介紹了如何在.Net Core 中使用配置,接下來將詳細介紹,這些都是如何實現的,先來看一張四大配置對象的UML圖

  • IConfigurationBuilder:整個配置系統的核心,包含多個IConfigurationSource,利用它們產生多個IConfigurationProvider,最終是為了得到一個IConfigurationRoot對象,並將它注入到容器中。
  • IConfigurationProvider:實際包含配置信息的類,內部包含一個字符串字典,它由IConfigurationSource產生。
  • IConfigurationSource:包含了一個配置源的信息,以文件為例,包含了文件名和文件路徑等,並不包含實際的配置信息。如果是基於數據庫的數據源,它會包含數據庫連接信息,SQL等。它的目的是為了產生一個IConfigurationProvider。
  • IConfigurationRoot:在獲取配置信息時,直接操作的對象,內部包含一個IConfigurationProvider列表。

在實際的使用過程中,我們首先都是調用各類數據源類庫提供的擴展方法往IConfigurationBuilder添加數據源IConfigurationSource,之后IConfigurationBuilder會依次調用IConfigurationSource的Build方法產生對應的IConfigurationProvider,並將他們傳入到IConfigurationRoot中,最終我們會拿到IConfigurationRoot進行使用。下面來依次看一看這幾個類的核心實現(以下的代碼都不是全部實現,只拿出了一部分)。

ConfigurationBuilder

public class ConfigurationBuilder : IConfigurationBuilder
    {
        public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();


        public IConfigurationBuilder Add(IConfigurationSource source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            Sources.Add(source);
            return this;
        }


        public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (var source in Sources)
            {
                var provider = source.Build(this);
                providers.Add(provider);
            }
            return new ConfigurationRoot(providers);
        }
    }

ConfigurationProvider

    public abstract class ConfigurationProvider : IConfigurationProvider
    {
        protected ConfigurationProvider()
        {
            Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        }

        protected IDictionary<string, string> Data { get; set; }

        public virtual bool TryGet(string key, out string value)
            => Data.TryGetValue(key, out value);

        public virtual void Set(string key, string value)
            => Data[key] = value;

        public virtual void Load()
        { }
    }

ConfigurationProvider系統的基礎實現是一個抽象類,具體的需要由對應類型的Provider去實現,其實也只是需要實現Load方法,去填充那個字符串字典。那應該如何實現這個字典呢,這個時候我們需要使用ConfigurationPath這個靜態類來幫助我們生成字典里的Key。具體來說就是各層級之間用分號(:)隔開,例如:A:B:C。這個分號是以靜態只讀的形式定義在ConfigurationPath中,最后我們可以看看我們之前定義的config01.json文件最終生成的字典結構,如下:

 

ConfigurationRoot

    public class ConfigurationRoot : IConfigurationRoot
    {
        private IList<IConfigurationProvider> _providers;public ConfigurationRoot(IList<IConfigurationProvider> providers)
        {
            if (providers == null)
            {
                throw new ArgumentNullException(nameof(providers));
            }

            _providers = providers;
            foreach (var p in providers)
            {
                p.Load();
            }
        }

        public IEnumerable<IConfigurationProvider> Providers => _providers;

        public string this[string key]
        {
            get
            {
                foreach (var provider in _providers.Reverse())
                {
                    string value;

                    if (provider.TryGet(key, out value))
                    {
                        return value;
                    }
                }

                return null;
            }

            set
            {
                if (!_providers.Any())
                {
                    throw new InvalidOperationException("Error");
                }

                foreach (var provider in _providers)
                {
                    provider.Set(key, value);
                }
            }
        }
public IConfigurationSection GetSection(string key) 
            => new ConfigurationSection(this, key);

    }

在這里我們可以看到,在構建的時候會依次調用傳遞過來的Provider的load方法去加載數據,而在取的數據的時候,會逆序依次調用Provider的TryGet方法獲取數據,如果成功就直接返回,這就是為什么后添加的數據源會覆蓋之前添加的數據源。

同時在創建ConfigurationSection時候會把ConfigurationRoot傳遞進去,而ConfigurationSection取數據的時候也是調用ConfigurationRoot的Get方法,實際ConfigurationSection也並不包含任何實際的配置數據。

最后,對於可監控的文件源,是基於FileWatch來實現的,核心代碼:

            if (Source.ReloadOnChange && Source.FileProvider != null)
            {
                ChangeToken.OnChange(
                    () => Source.FileProvider.Watch(Source.Path),
                    () => {
                        Thread.Sleep(Source.ReloadDelay);
                        Load(reload: true);
                    });
            }

 

示例中用到的Demo Code和UML圖,以及相關的源碼都在 https://github.com/zhurongbo111/AspNetCoreDemo/tree/master/01-Configuration


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM