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

1. 歡呼 .NET Standard 時代

我現在已不大提 .Net Core,對于我來說,未來的開發將是基于 .NET Standard,不僅僅是 面向未來 ,也是 面向過去;不只是 .Net Core 可以享受便利, .NET Framework 不升級一樣能享受 .NET Standard 帶來的好處。(目前 .NET Standard 支持 .NET Framework 4.6.1+)

2. 傳統配置的不足

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

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

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

3. 初識 IConfiguration

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

如前面提到的,這不是 .Net Core 的專屬。我們首先創建一個基于 .NET Framework 4.6.1 的控制臺應用 ( 代碼地址),然后安裝我們所需要的依賴。

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

然后引入我們的配置文件 my.conf:

{
  "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 文件等多種配置來源,同時社區的開源帶來了更多可能性,還支持諸如 consul、etcdapollo分布式配置中心

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

4. 配置防腐層

雖然微軟自帶的 IConfiguration 已經足夠用了,但是讓我們暢享下未來,或者回到我讓我困惑的問題。是不是所有的配置都將基于 IConfiguration ? 答案自然是否定的,編程技術不停地在發展,即使老而彌堅的 AppSetting 也難逃被淘汰的一天。所以為了讓我們的架構更長遠一些,我們需要進行 防腐層的設計。而且,如果你還在維護以前的老項目時,你更是需要借助防腐層的魔法去抵消同事或者上司的顧慮。

讓我們重新審視配置的用法,無非就是從某個 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 帶來的便利。


在本文的撰寫過程中,可能會存在疏漏,但我會盡量及時做出一些增刪改,所以如果是在轉載上看到的,內容可能是過時的,還請移步 我的博客 ,同時本文的 示例代碼 也會做相應的修改。
本文采用 知識共享署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議
轉載請注明來源:張蘅水

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容