.NET Core、.NET Standard之细说.NET配置

一、欢呼 .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 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();

 

迈向现代化的.NET配置指北

 

微软 支持 的来源除了有内存来源、还有系统变量、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<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

(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<IConfiguration>().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<Startup>();
}

 

同时由于 .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 中。

 

迈向现代化的.NET配置指北

 

另一种就是创建一个 key 的目录,然后每个 Section 分开配置。

 

迈向现代化的.NET配置指北

未经允许不得转载:996ICU » .NET Core、.NET Standard之细说.NET配置

赞 (0) 打赏