我如今已不大提 .Net Core,對於我來講,將來的開發將是基於 .NET Standard,不只僅是 面向將來 ,也是 面向過去;不僅是 .Net Core 能夠享受便利, .NET Framework 不升級同樣能享受 .NET Standard 帶來的好處。(目前 .NET Standard 支持 .NET Framework 4.6.1+)git
在我剛步足 .Net 的世界時,曾經有過一個 困惑,是否是全部的配置都必須寫在 Web.Config
中?而直到開始學習 .Net Core 的配置模式,才意識到傳統配置的不足:github
int
),而不僅是 string
很顯然微軟也意識到這些問題,而且設計出了一個強大而且客製化的配置方式,可是這也意味着從 AppSettings
中取出配置的時代也一去不復返。docker
在開始探討現代化配置設計以前,咱們先快速上手 .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();
微軟 支持 的來源除了有內存來源、還有系統變量、Json 文件、XML 文件等多種配置來源,同時社區的開源帶來了更多可能性,還支持諸如 consul、etcd 和 apollo 等 分佈式配置中心。微信
除了支持更多的配置來源外,咱們還觀察到,來源是否能夠 缺省 、是否能夠 重載 ,都是能夠配置的。特別是自動重載,這在 .NETFramework 時代是沒法想象的,每當咱們修改 Web.config
的配置文件時,熱心的 IIS 就會自動幫咱們重啓應用,而用戶在看到 500 的提示或者一片空白時,不由會發出這網站真爛的讚美。(同時須要注意配置 iis 的安全,避免能夠直接訪問配置的 json 文件,最好的方法是把json後綴改成諸如 conf 等)架構
雖然微軟自帶的 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
等一系列的只有接口的抽象基庫。
雖然咱們已經有了防腐層,但顯然咱們還沒考慮到實際的用法,特別是若是你的應用尚未引入依賴注入的支持,咱們前面實現的防腐層對於你來講,就是摸不着頭腦。同時,我仍是很喜歡之前那種直接從 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
來獲取咱們想要的配置了。
如今假設咱們擺脫了蠻荒時代,有了依賴注入的武器,使用配置最方便的用法莫不過直接注入一個配置對象,在 .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 用法不同? 儘管它在官方文檔備受到推崇,然而在實際開發中,我是幾乎不會使用到的,在我看來:
在微服務應用流行的今天,咱們須要的配置類會愈來愈多。咱們不停地注入,最終累死編輯器,是否有自動化注入的方法來解放咱們的鍵盤?答案天然是有的,然而在動手實現以前,咱們須要立下 約定優於配置 的海誓山盟。
首先,對於全部的配置類,他們均可以看做是一類或者某個接口的實現。
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; }
僅僅用了類的全名還不夠體現 約定優於配置 的威力,聯繫現實,是否是配置的某些選項是有默認值的,好比 TestConfig
的 DefauleVaule
。在沒有配置 DefauleVaule
的狀況下,DefauleVaule
的值將爲 默認值 ,即咱們代碼中的 "Hello World" ,反之設置了 DefauleVaule
則會覆蓋掉原來的默認值。
在微服務流行的今天,若是仍是像之前同樣人工改動配置文件,那是十分麻煩並且容易出錯的一件事情,這就須要引入配置中心,同時配置中心也必須是分佈式的,才能避免單點故障。
Consul 目前是個人首選方案,首先它足夠簡單,部署方便,同時已經夠用了。若是你還使用過 Consul,可使用 Docker 一鍵部署:
docker run -d -p 8500:8500 --name consul consul
而後在應用入口項目中引入 Winton.Extensions.Configuration.Consul的依賴。由於是我的開源,因此不免會有一些問題,好比我裝的版本就是 2.1.0-master0003
,它解決了 2.0.1
中的一些問題,但尚未發佈正式版。
若是你是 .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.cs
的 Configure
中處理:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime) { appLifetime.ApplicationStopping.Register(Program.ConfigCancellationTokenSource.Cancel); }
同理,對於 .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(); } }
咱們所說的配置,對於 Consul 來講,就是 Key/Value 。咱們有兩種配置,一種是把之前的json配置文件都寫到一個key 中。
另外一種就是建立一個 key 的目錄,而後每一個 Section 分開配置。
寫這篇文章很大的動力是看到很多 .Net Core 初學者抱怨使用配置中的各類坑,抱怨微軟文檔不夠清晰,同時也算是我兩年來的一些開發經驗總結。
最後,須要談一下感想。感覺最多的莫過於 .Net Core 開源帶來的衝擊,有不少開發者興致勃勃地想要把傳統的項目重構成 .Net Core 項目,然而思想卻沒有升級上去,反而越以爲 .Net Core 各類不適。可是隻要思想升級了,即便開發 .NET Framework 應用, 同樣也是能享受 .NET Standard 帶來的便利。
在本文的撰寫過程當中,可能會存在疏漏,但我會盡可能及時作出一些增刪改,因此若是是在轉載上看到的,內容多是過期的,還請移步 個人博客,同時本文的示例代碼也會作相應的修改。