【長期更新】邁向現代化的 .Net 配置指北

1. 歡呼 .NET Standard 時代

我如今已不大提 .Net Core,對於我來講,將來的開發將是基於 .NET Standard,不只僅是 面向將來 ,也是 面向過去;不僅是 .Net Core 能夠享受便利, .NET Framework 不升級同樣能享受 .NET Standard 帶來的好處。(目前 .NET Standard 支持 .NET Framework 4.6.1+)git

2. 傳統配置的不足

在我剛步足 .Net 的世界時,曾經有過一個 困惑,是否是全部的配置都必須寫在 Web.Config 中?而直到開始學習 .Net Core 的配置模式,才意識到傳統配置的不足:github

  • 除了 XML ,咱們可能還須要更多的配置來源支持,好比 Json
  • 配置是否能夠直接序列化成對象或者多種類型(直接取出來就是 int),而不僅是 string
  • 修改配置後,IIS 就重啓了,是否有辦法不重啓就能修改配置
  • 微服務(或者說分佈式)應用下管理配置帶來的困難

很顯然微軟也意識到這些問題,而且設計出了一個強大而且客製化的配置方式,可是這也意味着從 AppSettings 中取出配置的時代也一去不復返。docker

3. 初識 IConfiguration

在開始探討現代化配置設計以前,咱們先快速上手 .Net Core 中自帶的 Microsoft.Extensions.Configuration編程

如前面提到的,這不是 .Net Core 的專屬。咱們首先建立一個基於 .NET Framework 4.6.1 的控制檯應用 ( 代碼地址),而後安裝咱們所須要的依賴。json

Nuget Install Microsoft.Extensions.Configuration.Json
Nuget Install Microsoft.Extensions.Configuration.Binder

而後引入咱們的配置文件 my.conf:api

{
  "TestConfig": {
    "starship": {
      "name": "USS Enterprise",
      "registry": "NCC-1701",
      "class": "Constitution",
      "length": 304.8,
      "commissioned": false
    },
    "trademark": "Paramount Pictures Corp. http://www.paramount.com"
  }
}

最後,輸入以下的代碼,並啓動:安全

var configurationBuilder = new ConfigurationBuilder().AddJsonFile("my.conf", optional: true, reloadOnChange: true)
                                .AddInMemoryCollection(new List<KeyValuePair<String, String>>
                {
                    new KeyValuePair<String,String>("myString","myString"),
                    new KeyValuePair<String,String>("otherString","otherString")
                });
            IConfiguration config = configurationBuilder.Build();
            String myString = config["myString"]; //myString
            TestConfig testConfig = config.GetSection("TestConfig").Get<TestConfig>();
            var length = testConfig.Starship.Length;//304.8
            Console.WriteLine($"myString:{myString}");
            Console.WriteLine($"myString:{JsonConvert.SerializeObject(testConfig)}");
            Console.ReadKey();

微信截圖_20180908172500.png

微軟 支持 的來源除了有內存來源、還有系統變量Json 文件、XML 文件等多種配置來源,同時社區的開源帶來了更多可能性,還支持諸如 consuletcdapollo分佈式配置中心微信

除了支持更多的配置來源外,咱們還觀察到,來源是否能夠 缺省 、是否能夠 重載 ,都是能夠配置的。特別是自動重載,這在 .NETFramework 時代是沒法想象的,每當咱們修改 Web.config的配置文件時,熱心的 IIS 就會自動幫咱們重啓應用,而用戶在看到 500 的提示或者一片空白時,不由會發出這網站真爛的讚美。(同時須要注意配置 iis 的安全,避免能夠直接訪問配置的 json 文件,最好的方法是把json後綴改成諸如 conf 等)架構

4. 配置防腐層

雖然微軟自帶的 IConfiguration 已經足夠用了,可是讓咱們暢享下將來,或者回到我讓我困惑的問題。是否是全部的配置都將基於 IConfiguration ? 答案天然是否認的,編程技術不停地在發展,即便老而彌堅的 AppSetting 也難逃被淘汰的一天。因此爲了讓咱們的架構更長遠一些,咱們須要進行 防腐層的設計。並且,若是你還在維護之前的老項目時,你更是須要藉助防腐層的魔法去抵消同事或者上司的顧慮。app

讓咱們從新審視配置的用法,無非就是從某個 key 獲取對應的值(多是字符串、也多是個對象),因此咱們能夠在最底層的類庫或全局類庫中定義一個 IConfigurationGeter 來知足咱們的要求。

namespace ZHS.Configuration.Core

public interface IConfigurationGeter
 {
    TConfig Get<TConfig>(string key);
    String this[string key] { get;}
}

而關於 IConfigurationGeter的實現,咱們姑且叫它 ConfigurationGetter ,基於防腐層的設計,咱們不能在底層的類庫安裝任何依賴。因此咱們須要新建一個基礎設施層或者在應用入口層實現。(代碼示例中能夠看到是在不一樣的項目中)

namespace ZHS.Configuration.DotNetCore

   public class ConfigurationGetter : IConfigurationGeter
    {
        private readonly IConfiguration _configuration;

        public ConfigurationGetter(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public TConfig Get<TConfig>(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
                throw new ArgumentException("Value cannot be null or whitespace.", nameof(key));
            var section = _configuration.GetSection(key);
            return section.Get<TConfig>();
        }
        public string this[string key] => _configuration[key];
    }

之後咱們全部的配置都是經過 IConfigurationGeter 獲取,這樣就避免了在你的應用層(或者三層架構中的 BAL 層) 中引入 Microsoft.Extensions.Configuration 的依賴。固然可能有些人會以爲大材小用,但實際上等你到了真正的開發,你就會以爲其中的好處。不止是我,.Net Core 的設計者早就意識到防腐層的重要性,因此纔會有 Microsoft.Extensions.Configuration.Abstractions 等一系列的只有接口的抽象基庫。

5. 靜態獲取配置

雖然咱們已經有了防腐層,但顯然咱們還沒考慮到實際的用法,特別是若是你的應用尚未引入依賴注入的支持,咱們前面實現的防腐層對於你來講,就是摸不着頭腦。同時,我仍是很喜歡之前那種直接從 AppSetting 中取出配置的便捷。因此,這裏咱們須要引入 服務定位器模式 來知足 靜態獲取配置 的便捷操做。

namespace ZHS.Configuration.Core

public class ConfigurationGeterLocator
{
   private readonly IConfigurationGeter _currentServiceProvider;

   private static IConfigurationGeter _serviceProvider;

    public ConfigurationGeterLocator(IConfigurationGeter currentServiceProvider)
    {
      _currentServiceProvider = currentServiceProvider;
    }

    public static ConfigurationGeterLocator Current => new ConfigurationGeterLocator(_serviceProvider);

    public static void SetLocatorProvider(IConfigurationGeter serviceProvider)
    {
     _serviceProvider = serviceProvider;
    }

    public TConfig Get<TConfig>(String key)
     {
       return _currentServiceProvider.Get<TConfig>(key);
     }

     public  String this[string key] => _currentServiceProvider[key];
}
public static IConfiguration AddConfigurationGeterLocator(this IConfiguration configuration)
        {
            ConfigurationGeterLocator.SetLocatorProvider(new ConfigurationGetter(configuration));
            return configuration;
        }

作完這些基礎工做,咱們還須要在應用入口函數念一句咒語讓他生效。

config.AddConfigurationGeterLocator();
var myString = ConfigurationGeterLocator.Current["myString"];// "myString"

如今,咱們就能像之前同樣,直接調用 ConfigurationGeterLocator.Current 來獲取咱們想要的配置了。

6. 依賴注入的曙光

如今假設咱們擺脫了蠻荒時代,有了依賴注入的武器,使用配置最方便的用法莫不過直接注入一個配置對象,在 .Net Core 中作法大體以下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<TestConfig>(provider =>Configuration.GetSection("TestConfig").Get<TestConfig>());
}

而它的使用就十分方便:

public class ValuesController : ControllerBase
    {
        private readonly TestConfig _testConfig;

        public ValuesController(TestConfig testConfig)
        {
            _testConfig = testConfig;
        }

        // GET api/values
        [HttpGet]
        public JsonResult Get()
        {
            var data = new
            {
               TestConfig = _testConfig
            };
            return new JsonResult(data);
        }
    }

看到這裏你可能會困惑,怎麼和官方推薦的 IOptions 用法不同? 儘管它在官方文檔備受到推崇,然而在實際開發中,我是幾乎不會使用到的,在我看來:

  • 不使用 IOptions 就已經獲得了對應的效果
  • 使用 IOptionsSnapshot 才能約束配置是否須要熱重載,但實際這個並很差控制(因此雞肋)
  • 咱們已經有防腐層了,再引入就是破壞了設計

7. 約定優於配置的福音

在微服務應用流行的今天,咱們須要的配置類會愈來愈多。咱們不停地注入,最終累死編輯器,是否有自動化注入的方法來解放咱們的鍵盤?答案天然是有的,然而在動手實現以前,咱們須要立下 約定優於配置 的海誓山盟。

首先,對於全部的配置類,他們均可以看做是一類或者某個接口的實現。

public interface IConfigModel{ }

public class TestConfig : IConfigModel
 {
     public String DefauleVaule { get; set; } = "Hello World";
     public Starship Starship { get; set; }
     public string Trademark { get; set; }
}

public class Starship
{
    public string Name { get; set; }
    public string Registry { get; set; }
    public string Class { get; set; }
    public float Length { get; set; }
    public bool Commissioned { get; set; }
}

聯想咱們剛剛注入 TestConfig 的時候,是否是指定了配置節點 "TestConfig" ,那麼若是咱們要自動注入的話,是否是能夠考慮直接使用類的惟一標誌,好比類的全名,那麼注入的方法就能夠修改成以下:

public static IServiceCollection AddConfigModel(this IServiceCollection services)
{
          var types = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(a => a.GetTypes().Where(t => t.GetInterfaces().Contains(typeof(IConfigModel))))
                .ToArray();

          foreach (var type in types)
            {
                services.AddScoped(type, provider =>
                {
                    var config = provider.GetService<IConfiguration>().GetSection(type.FullName).Get(type);
                    return config;
                });
            }
            return services;
}

僅僅用了類的全名還不夠體現 約定優於配置 的威力,聯繫現實,是否是配置的某些選項是有默認值的,好比 TestConfigDefauleVaule 。在沒有配置 DefauleVaule 的狀況下,DefauleVaule 的值將爲 默認值 ,即咱們代碼中的 "Hello World" ,反之設置了 DefauleVaule 則會覆蓋掉原來的默認值。

8. 分佈式配置中心

在微服務流行的今天,若是仍是像之前同樣人工改動配置文件,那是十分麻煩並且容易出錯的一件事情,這就須要引入配置中心,同時配置中心也必須是分佈式的,才能避免單點故障。

8.1 Consul

Consul 目前是個人首選方案,首先它足夠簡單,部署方便,同時已經夠用了。若是你還使用過 Consul,可使用 Docker 一鍵部署:

docker run -d -p 8500:8500  --name consul  consul

而後在應用入口項目中引入 Winton.Extensions.Configuration.Consul的依賴。由於是我的開源,因此不免會有一些問題,好比我裝的版本就是 2.1.0-master0003,它解決了 2.0.1 中的一些問題,但尚未發佈正式版。

8.1.1 .Net Core 使用 Consul 配置中心

若是你是 .Net Core 應用,你須要在 Program.cs 配置 ConfigureAppConfiguration:

public class Program
    {
        public static readonly CancellationTokenSource ConfigCancellationTokenSource = new CancellationTokenSource();

        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((builderContext, config) =>
                {
                    IHostingEnvironment env = builderContext.HostingEnvironment;
                    var tempConfigBuilder = config;
                    var key = $"{env.ApplicationName}.{env.EnvironmentName}";//ZHS.Configuration.DotNetCore.Consul.Development
                    config.AddConsul(key, ConfigCancellationTokenSource.Token, options =>
                    {
                        options.ConsulConfigurationOptions =
                            co => { co.Address = new Uri("http://127.0.0.1:8500"); };
                        options.ReloadOnChange = true;
                        options.Optional = true;
                        options.OnLoadException = exceptionContext =>
                        {
                            exceptionContext.Ignore = true;
                        };
                    });
                })
                .UseStartup<Startup>();
    }

同時因爲 .Net 客戶端與 Consul 之間交互會使用長輪詢,因此咱們須要在關閉應用的同時也要記得把鏈接回收,這就須要在 Startup.csConfigure 中處理:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime)
 {
     appLifetime.ApplicationStopping.Register(Program.ConfigCancellationTokenSource.Cancel);
 }

8.1.2 .NET Framework 使用 Consul 配置中心

同理,對於 .NET Framework 應用來講,也是須要作對應的處理,在 Global.asax 中:

public class WebApiApplication : System.Web.HttpApplication
    {
        public static readonly CancellationTokenSource ConfigCancellationTokenSource = new CancellationTokenSource();

        protected void Application_Start()
        {
            AddConsul();
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }

        private static void AddConsul()
        {
            var config = new ConfigurationBuilder();
            config.AddConsul("ZHS.Configuration.DotNetCore.Consul.Development", ConfigCancellationTokenSource.Token, options =>
            {
                options.ConsulConfigurationOptions =
                    co => { co.Address = new Uri("http://127.0.0.1:8500"); };
                options.ReloadOnChange = true;
                options.Optional = true;
                options.OnLoadException = exceptionContext =>
                {
                    exceptionContext.Ignore = true;
                };
            });
            //var test = config.Build();
            config.Build().AddConfigurationGeterLocator();
        }

        protected void Application_End(object sender, EventArgs e)
        {
            ConfigCancellationTokenSource.Cancel();
        }
    }

8.1.3 配置 Consul

咱們所說的配置,對於 Consul 來講,就是 Key/Value 。咱們有兩種配置,一種是把之前的json配置文件都寫到一個key 中。

單個key包含所有配置

另外一種就是建立一個 key 的目錄,而後每一個 Section 分開配置。

分開配置

9. 結語

寫這篇文章很大的動力是看到很多 .Net Core 初學者抱怨使用配置中的各類坑,抱怨微軟文檔不夠清晰,同時也算是我兩年來的一些開發經驗總結。

最後,須要談一下感想。感覺最多的莫過於 .Net Core 開源帶來的衝擊,有不少開發者興致勃勃地想要把傳統的項目重構成 .Net Core 項目,然而思想卻沒有升級上去,反而越以爲 .Net Core 各類不適。可是隻要思想升級了,即便開發 .NET Framework 應用, 同樣也是能享受 .NET Standard 帶來的便利。


在本文的撰寫過程當中,可能會存在疏漏,但我會盡可能及時作出一些增刪改,因此若是是在轉載上看到的,內容多是過期的,還請移步 個人博客,同時本文的示例代碼也會作相應的修改。

相關文章
相關標籤/搜索