1. 程式人生 > >【長期更新】邁向現代化的 .Net 配置指北

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

同時 lis isn 創建 ONBUILD 現實 入口 會有 hosting

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();

技術分享圖片

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

除了支持更多的配置來源外,我們還觀察到,來源是否可以 缺省 、是否可以 重載 ,都是可以配置的。特別是自動重載,這在 .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>();
    }

同時因為是客戶端與 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 的目錄,然後每個 Section 分開配置。

技術分享圖片

9. 結語

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

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


在本文的撰寫過程中,可能會存在疏漏,但我會盡量及時做出一些增刪改,所以如果是在轉載上看到的,內容可能是過時的,還請移步 我的博客,同時本文的示例代碼也會做相應的修改。

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