邁向現(xiàn)代化的 .Net 配置指北
1. 歡呼 .NET Standard 時(shí)代
我現(xiàn)在已不大提 .Net Core,對(duì)于我來(lái)說(shuō),未來(lái)的開發(fā)將是基于 .NET Standard,不僅僅是 面向未來(lái) ,也是 面向過(guò)去;不只是 .Net Core 可以享受便利, .NET Framework 不升級(jí)一樣能享受 .NET Standard 帶來(lái)的好處。(目前 .NET Standard 支持 .NET Framework 4.6.1+)
2. 傳統(tǒng)配置的不足
在我剛步足 .Net 的世界時(shí),曾經(jīng)有過(guò)一個(gè) 困惑,是不是所有的配置都必須寫在 Web.Config 中?而直到開始學(xué)習(xí) .Net Core 的配置模式,才意識(shí)到傳統(tǒng)配置的不足:
- 除了 XML ,我們可能還需要更多的配置來(lái)源支持,比如 Json
- 配置是否可以直接序列化成對(duì)象或者多種類型(直接取出來(lái)就是
int),而不只是string - 修改配置后,IIS 就重啟了,是否有辦法不重啟就能修改配置
- 微服務(wù)(或者說(shuō)分布式)應(yīng)用下管理配置帶來(lái)的困難
很顯然微軟也意識(shí)到這些問(wèn)題,并且設(shè)計(jì)出了一個(gè)強(qiáng)大并且客制化的配置方式,但是這也意味著從 AppSettings 中取出配置的時(shí)代也一去不復(fù)返。
3. 初識(shí) IConfiguration
在開始探討現(xiàn)代化配置設(shè)計(jì)之前,我們先快速上手 .Net Core 中自帶的 Microsoft.Extensions.Configuration。
如前面提到的,這不是 .Net Core 的專屬。我們首先創(chuàng)建一個(gè)基于 .NET Framework 4.6.1 的控制臺(tái)應(yīng)用 ( 代碼地址),然后安裝我們所需要的依賴。
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"
}
}
最后,輸入如下的代碼,并啟動(dòng):
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();

微軟 支持 的來(lái)源除了有內(nèi)存來(lái)源、還有系統(tǒng)變量、Json 文件、XML 文件等多種配置來(lái)源,同時(shí)社區(qū)的開源帶來(lái)了更多可能性,還支持諸如 consul、etcd 和 apollo 等 分布式配置中心。
除了支持更多的配置來(lái)源外,我們還觀察到,來(lái)源是否可以 缺省 、是否可以 重載 ,都是可以配置的。特別是自動(dòng)重載,這在 .NETFramework 時(shí)代是無(wú)法想象的,每當(dāng)我們修改 Web.config的配置文件時(shí),熱心的 IIS 就會(huì)自動(dòng)幫我們重啟應(yīng)用,而用戶在看到 500 的提示或者一片空白時(shí),不禁會(huì)發(fā)出這網(wǎng)站真爛的贊美。(同時(shí)需要注意配置 iis 的安全,避免可以直接訪問(wèn)配置的 json 文件,最好的方法是把json后綴改為諸如 conf 等)
4. 配置防腐層
雖然微軟自帶的 IConfiguration 已經(jīng)足夠用了,但是讓我們暢享下未來(lái),或者回到我讓我困惑的問(wèn)題。是不是所有的配置都將基于 IConfiguration ? 答案自然是否定的,編程技術(shù)不停地在發(fā)展,即使老而彌堅(jiān)的 AppSetting 也難逃被淘汰的一天。所以為了讓我們的架構(gòu)更長(zhǎng)遠(yuǎn)一些,我們需要進(jìn)行 防腐層的設(shè)計(jì)。而且,如果你還在維護(hù)以前的老項(xiàng)目時(shí),你更是需要借助防腐層的魔法去抵消同事或者上司的顧慮。
讓我們重新審視配置的用法,無(wú)非就是從某個(gè) key 獲取對(duì)應(yīng)的值(可能是字符串、也可能是個(gè)對(duì)象),所以我們可以在最底層的類庫(kù)或全局類庫(kù)中定義一個(gè) IConfigurationGeter 來(lái)滿足我們的要求。
namespace ZHS.Configuration.Core
public interface IConfigurationGeter
{
TConfig Get<TConfig>(string key);
String this[string key] { get;}
}
而關(guān)于 IConfigurationGeter 的實(shí)現(xiàn),我們姑且叫它 ConfigurationGetter ,基于防腐層的設(shè)計(jì),我們不能在底層的類庫(kù)安裝任何依賴。所以我們需要新建一個(gè)基礎(chǔ)設(shè)施層或者在應(yīng)用入口層實(shí)現(xiàn)。(代碼示例中可以看到是在不同的項(xiàng)目中)
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];
}
以后我們所有的配置都是通過(guò) IConfigurationGeter 獲取,這樣就避免了在你的應(yīng)用層(或者三層架構(gòu)中的 BAL 層) 中引入 Microsoft.Extensions.Configuration 的依賴。當(dāng)然可能有些人會(huì)覺得大材小用,但實(shí)際上等你到了真正的開發(fā),你就會(huì)覺得其中的好處。不止是我,.Net Core 的設(shè)計(jì)者早就意識(shí)到防腐層的重要性,所以才會(huì)有 Microsoft.Extensions.Configuration.Abstractions 等一系列的只有接口的抽象基庫(kù)。
5. 靜態(tài)獲取配置
雖然我們已經(jīng)有了防腐層,但顯然我們還沒考慮到實(shí)際的用法,特別是如果你的應(yīng)用還沒有引入依賴注入的支持,我們前面實(shí)現(xiàn)的防腐層對(duì)于你來(lái)說(shuō),就是摸不著頭腦。同時(shí),我還是很喜歡以前那種直接從 AppSetting 中取出配置的便捷。所以,這里我們需要引入 服務(wù)定位器模式 來(lái)滿足 靜態(tài)獲取配置 的便捷操作。
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;
}
做完這些基礎(chǔ)工作,我們還需要在應(yīng)用入口函數(shù)念一句咒語(yǔ)讓他生效。
config.AddConfigurationGeterLocator();
var myString = ConfigurationGeterLocator.Current["myString"];// "myString"
現(xiàn)在,我們就能像以前一樣,直接調(diào)用 ConfigurationGeterLocator.Current 來(lái)獲取我們想要的配置了。
6. 依賴注入的曙光
現(xiàn)在假設(shè)我們擺脫了蠻荒時(shí)代,有了依賴注入的武器,使用配置最方便的用法莫不過(guò)直接注入一個(gè)配置對(duì)象,在 .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);
}
}
看到這里你可能會(huì)困惑,怎么和官方推薦的 IOptions 用法不一樣? 盡管它在官方文檔備受到推崇,然而在實(shí)際開發(fā)中,我是幾乎不會(huì)使用到的,在我看來(lái):
- 不使用 IOptions 就已經(jīng)得到了對(duì)應(yīng)的效果
- 使用 IOptionsSnapshot 才能約束配置是否需要熱重載,但實(shí)際這個(gè)并不好控制(所以雞肋)
- 我們已經(jīng)有防腐層了,再引入就是破壞了設(shè)計(jì)
7. 約定優(yōu)于配置的福音
在微服務(wù)應(yīng)用流行的今天,我們需要的配置類會(huì)越來(lái)越多。我們不停地注入,最終累死編輯器,是否有自動(dòng)化注入的方法來(lái)解放我們的鍵盤?答案自然是有的,然而在動(dòng)手實(shí)現(xiàn)之前,我們需要立下 約定優(yōu)于配置 的海誓山盟。
首先,對(duì)于所有的配置類,他們都可以看作是一類或者某個(gè)接口的實(shí)現(xiàn)。
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; }
}
聯(lián)想我們剛剛注入 TestConfig 的時(shí)候,是不是指定了配置節(jié)點(diǎn) "TestConfig" ,那么如果我們要自動(dòng)注入的話,是不是可以考慮直接使用類的唯一標(biāo)志,比如類的全名,那么注入的方法就可以修改為如下:
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;
}
僅僅用了類的全名還不夠體現(xiàn) 約定優(yōu)于配置 的威力,聯(lián)系現(xiàn)實(shí),是不是配置的某些選項(xiàng)是有默認(rèn)值的,比如 TestConfig 的 DefauleVaule 。在沒有配置 DefauleVaule 的情況下,DefauleVaule 的值將為 默認(rèn)值 ,即我們代碼中的 "Hello World" ,反之設(shè)置了 DefauleVaule 則會(huì)覆蓋掉原來(lái)的默認(rèn)值。
8. 分布式配置中心
在微服務(wù)流行的今天,如果還是像以前一樣人工改動(dòng)配置文件,那是十分麻煩而且容易出錯(cuò)的一件事情,這就需要引入配置中心,同時(shí)配置中心也必須是分布式的,才能避免單點(diǎn)故障。
8.1 Consul
Consul 目前是我的首選方案,首先它足夠簡(jiǎn)單,部署方便,同時(shí)已經(jīng)夠用了。如果你還使用過(guò) Consul,可以使用 Docker 一鍵部署:
docker run -d -p 8500:8500 --name consul consul
然后在應(yīng)用入口項(xiàng)目中引入 Winton.Extensions.Configuration.Consul 的依賴。因?yàn)槭莻€(gè)人開源,所以難免會(huì)有一些問(wèn)題,比如我裝的版本就是 2.1.0-master0003,它解決了 2.0.1 中的一些問(wèn)題,但還沒有發(fā)布正式版。
8.1.1 .Net Core 使用 Consul 配置中心
如果你是 .Net Core 應(yīng)用,你需要在 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>();
}
同時(shí)由于 .Net 客戶端與 Consul 之間交互會(huì)使用長(zhǎng)輪詢,所以我們需要在關(guān)閉應(yīng)用的同時(shí)也要記得把連接回收,這就需要在 Startup.cs 的 Configure 中處理:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime)
{
appLifetime.ApplicationStopping.Register(Program.ConfigCancellationTokenSource.Cancel);
}
8.1.2 .NET Framework 使用 Consul 配置中心
同理,對(duì)于 .NET Framework 應(yīng)用來(lái)說(shuō),也是需要做對(duì)應(yīng)的處理,在 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
我們所說(shuō)的配置,對(duì)于 Consul 來(lái)說(shuō),就是 Key/Value 。我們有兩種配置,一種是把以前的json配置文件都寫到一個(gè)key 中。

另一種就是創(chuàng)建一個(gè) key 的目錄,然后每個(gè) Section 分開配置。

9. 結(jié)語(yǔ)
寫這篇文章很大的動(dòng)力是看到不少 .Net Core 初學(xué)者抱怨使用配置中的各種坑,抱怨微軟文檔不夠清晰,同時(shí)也算是我兩年來(lái)的一些開發(fā)經(jīng)驗(yàn)總結(jié)。
最后,需要談一下感想。感受最多的莫過(guò)于 .Net Core 開源帶來(lái)的沖擊,有很多開發(fā)者興致勃勃地想要把傳統(tǒng)的項(xiàng)目重構(gòu)成 .Net Core 項(xiàng)目,然而思想?yún)s沒有升級(jí)上去,反而越覺得 .Net Core 各種不適。但是只要思想升級(jí)了,即使開發(fā) .NET Framework 應(yīng)用, 一樣也是能享受 .NET Standard 帶來(lái)的便利。
在本文的撰寫過(guò)程中,可能會(huì)存在疏漏,但我會(huì)盡量及時(shí)做出一些增刪改,所以如果是在轉(zhuǎn)載上看到的,內(nèi)容可能是過(guò)時(shí)的,還請(qǐng)移步 我的博客 ,同時(shí)本文的 示例代碼 也會(huì)做相應(yīng)的修改。

浙公網(wǎng)安備 33010602011771號(hào)