歡迎光臨
每天分享高質量文章

邁向現代化的.NET配置指北

作者:張蘅水連結:http://www.cnblogs.com/chenug/p/9610172.html

一、歡呼 .NET Standard 時代

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

(目前 .NET Standard 支援 .NET Framework 4.6.1+)

二、傳統配置的不足

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

  • 除了 XML ,我們可能還需要更多的配置來源支援,比如 Json

  • 配置是否可以直接序列化成物件或者多種型別(直接取出來就是 int),而不只是 string

  • 修改配置後,IIS 就重啟了,是否有辦法不重啟就能修改配置

  • 微服務(或者說分散式)應用下管理配置帶來的困難

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

三、初識 IConfiguration

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

如前面提到的,這不是 .Net Core 的專屬。

我們首先建立一個基於 .NET Framework 4.6.1 的控制檯應用程式碼地址:(https://github.com/jonechenug/ZHS.Configuration.Sample/blob/master/src/ZHS.Configuration.NFX.Sample/Program.cs),然後安裝我們所需要的依賴。

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 ListString, 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();
            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 也難逃被淘汰的一天。所以為了讓我們的架構更長遠一些,我們需要進行 防腐層的設計。

而且,如果你還在維護以前的老專案時,你更是需要藉助防腐層的魔法去抵消同事或者上司的顧慮。

讓我們重新審視配置的用法,無非就是從某個 key 獲取對應的值(可能是字串、也可能是個物件),所以我們可以在最底層的類庫或全域性類庫中定義一個 IConfigurationGeter 來滿足我們的要求。

namespace ZHS.Configuration.Core
public interface IConfigurationGeter
{
    TConfig Get(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(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();
    }
    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(String key)
    {
       return _currentServiceProvider.Get(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(provider =>Configuration.GetSection("TestConfig").Get());
}

而它的使用就十分方便:

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

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.1 ) 用法不一樣? 儘管它在官方檔案備受到推崇,然而在實際開發中,我是幾乎不會使用到的,在我看來:

  • 不使用 IOptions就已經得到了對應的效果

  • 使用 IOptionsSnapshot 才能約束配置是否需要熱多載,但實際這個並不好控制(所以雞肋)

  • 我們已經有防腐層了,再引入就是破壞了設計

七、約定優於配置的福音

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

首先,對於所有的配置類,他們都可以看作是一類或者某個介面的實現。

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().GetSection(type.FullName).Get(type);
                return config;
            });
        }
        return services;
}

僅僅用了類的全名還不夠體現 約定優於配置 的威力,聯絡現實,是不是配置的某些選項是有預設值的,比如 TestConfig 的 DefauleVaule 。

在沒有配置 DefauleVaule 的情況下,DefauleVaule 的值將為 預設值 ,即我們程式碼中的 “Hello World” ,反之設定了 DefauleVaule 則會改寫掉原來的預設值。

八、分散式配置中心

在微服務流行的今天,如果還是像以前一樣人工改動配置檔案,那是十分麻煩而且容易出錯的一件事情,這就需要引入配置中心,同時配置中心也必須是分散式的,才能避免單點故障。

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 、.NETCore 使用 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();
}

同時由於 .Net 客戶端與 Consul 之間互動會使用長輪詢,所以我們需要在關閉應用的同時也要記得把連接回收,這就需要在 Startup.cs 的 Configure 中處理:

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 的目錄,然後每個 Section 分開配置。

九、結語

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

最後,需要談一下感想。感受最多的莫過於 .NET Core 開源帶來的衝擊,有很多開發者興緻勃勃地想要把傳統的專案重構成 .NET Core 專案,然而思想卻沒有升級上去,反而越覺得 .NET Core 各種不適。

但是隻要思想升級了,即使開發 .NET Framework 應用, 一樣也是能享受 .NET Standard 帶來的便利。

贊(0)

分享創造快樂