乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core配置框架,让服务无缝适应各种环境

ASP.NET Core配置框架

image

ASP.NET Core配置框架的核心组件包是如下两个:

  • Microsoft.Extensions.Configuration.Abstractions,配置框架抽象定义
  • Microsoft.Extensions.Configuration,配置框架默认实现。

image

获取安装配置框架包

https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Abstractions

dotnet add package Microsoft.Extensions.Configuration.Abstractions

image

https://www.nuget.org/packages/Microsoft.Extensions.Configuration

dotnet add package Microsoft.Extensions.Configuration

image

框架特点

  • 以Key-Value字符串的键值对方式抽象了配置。
  • 支持从各种不同的数据源读取配置。

核心类型

  • IConfiguration
  • IConfigurationRoot
  • IConfigurationSection
  • IConfigurationBuilder

框架扩展点

  • IConfigurationSource
  • IConfigurationProvider

实践理解

https://github.com/TaylorShi/HelloConfiguration

创建项目安装Nuget包

image

image

使用内存数据源(AddInMemoryCollection)

依赖包

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Abstractions

虽然配置是以Key-Value来组装配置,但是也可以通过分隔符:来分组。

// 创建配置构建器
IConfigurationBuilder builder = new ConfigurationBuilder();
// 添加内存数据源
builder.AddInMemoryCollection(new Dictionary<string, string>
{
    { "car1", "tesla"},
    { "car2", "benci"},
    { "group:car3", "byd"}
});
// 构建配置获取配置根
IConfigurationRoot configurationRoot = builder.Build();
// 获取并输出配置根下的值
Console.WriteLine(configurationRoot["car1"]);
Console.WriteLine(configurationRoot["car2"]);
// 获取并输出配置节点下的值
IConfigurationSection configurationSection = configurationRoot.GetSection("group");
Console.WriteLine($"car3:{configurationSection["car3"]}");
Console.WriteLine($"car4:{configurationSection["car4"]}");

运行效果:

tesla
benci
car3:byd
car4:

使用命令行数据源(AddCommandLine)

依赖包

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Abstractions
dotnet add package Microsoft.Extensions.Configuration.CommandLine

支持命令格式包括:

  • 无前缀的Key=Value模式。
  • 双中横线模式 --key=value 或 --key value
  • 正斜杆模式 /key=value 或 /key value

注意,等号分隔符和空格分隔符不能混用。

命令替换模式

  • 必须以单横杆(-)或双横杆(--)开头
  • 映射字典不能包含重复Key

配置调试启动命令行参数

CommandLineKey1=Value1 --CommandLineKey2=value2 /CommandLineKey3=value3 -k1=k3

image

launchSettings.json

{
  "profiles": {
    "demoForConsole31": {
      "commandName": "Project",
      "commandLineArgs": "CommandLineKey1=Value1 --CommandLineKey2=value2 /CommandLineKey3=value3 -k1=k3"
    }
  }
}

示例代码

// 创建配置构建器
IConfigurationBuilder builder = new ConfigurationBuilder();
var mapper = new Dictionary<string, string> { { "-k1", "CommandLineKey1" } };
// 添加命令行数据源
builder.AddCommandLine(args, mapper);
// 构建配置获取配置根
IConfigurationRoot configurationRoot = builder.Build();
// 获取并输出配置根下的值
Console.WriteLine(configurationRoot["CommandLineKey1"]);
Console.WriteLine(configurationRoot["CommandLineKey2"]);
Console.WriteLine(configurationRoot["CommandLineKey3"]);

因为这里-k1单横杆模式代表别名,这里我们设计了一个Mapper,指定了这个-k1就是指代了CommandLineKey1

运行结果:

k3
value2
value3

虽然CommandLineKey1最初的值是value1,但是后面它又被-k1这个别名替代了,所以它的值最终是-k带进来的值,也就是k3

Dotnet Cli官方的示例

image

这里的-h就代表了--help

使用环境变量数据源(AddEnvironmentVariables)

依赖包

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Abstractions
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables

适用场景

  • 在Docker中运行中。
  • 在Kubernetes中运行时。
  • 需要设置ASP.NET Core的一些内置特殊配置时。

特性

  • 对于配置的分层键,支持用双下横线__替代:
  • 支持根据前缀加载

配置调试启动环境变量

image

launchSettings.json

{
  "profiles": {
    "demoForConsole31": {
      "commandName": "Project",
      "environmentVariables": {
        "car1": "tesla",
        "car2": "byd",
        "car__car3": "benci"
      }
    }
  }
}

示例代码

// 创建配置构建器
IConfigurationBuilder builder = new ConfigurationBuilder();
// 添加环境变量数据源
builder.AddEnvironmentVariables();
// 构建配置获取配置根
IConfigurationRoot configurationRoot = builder.Build();
// 获取并输出配置根下的值
Console.WriteLine(configurationRoot["car1"]);
Console.WriteLine(configurationRoot["car2"]);
// 单级分层
Console.WriteLine(configurationRoot.GetSection("car")["car3"]);
// 多级分层
Console.WriteLine(configurationRoot.GetSection("car4:car5")["car6"]);

运行结果:

tesla
byd
benci
baoma

这里car通过分隔符__来分层了。

前缀过滤

// 创建配置构建器
IConfigurationBuilder builder = new ConfigurationBuilder();
// 添加环境变量数据源并且过滤前缀
builder.AddEnvironmentVariables("car_");
// 构建配置获取配置根
IConfigurationRoot configurationRoot = builder.Build();
// 获取并输出配置根下的值
Console.WriteLine(configurationRoot["car_xxx"]);
{
    "environmentVariables": {
        "car1": "tesla",
        "car2": "byd",
        "car__car3": "benci",
        "car4__car5__car6": "baoma",
        "car_xxx": "jieguan"
    }
}

运行结果:

理论上应该是

jieguan

但运行不显示,还不知道为何?

文件配置

提供程序

  • Microsoft.Extensions.Configuration.Json
  • Microsoft.Extensions.Configuration.Ini
  • Microsoft.Extensions.Configuration.Xml
  • Microsoft.Extensions.Configuration.UserSecrets
  • Microsoft.Extensions.Configuration.NewtonsoftJson(已废弃)

特性

  • 指定文件可选、必选
  • 指定是否监视文件的变更

使用Json文件配置(AddJsonFile)

依赖包

dotnet add package Microsoft.Extensions.Configuration.Json

添加Json配置文件(appsettings.json)

{
  "car1": "tesla",
  "car2": "byd",
  "car3": "baoma",
  "car4": 10,
  "car5": false
}

示例代码

// 创建配置构建器
IConfigurationBuilder builder = new ConfigurationBuilder();
// 添加Json文件数据源
builder.AddJsonFile("appsettings.json");
// 构建配置获取配置根
IConfigurationRoot configurationRoot = builder.Build();
// 获取并输出配置根下的值
Console.WriteLine(configurationRoot["car1"]);
Console.WriteLine(configurationRoot["car2"]);
Console.WriteLine(configurationRoot["car3"]);
Console.WriteLine(configurationRoot["car4"]);
Console.WriteLine(configurationRoot["car5"]);

运行结果

tesla
byd
baoma
10
False

设置文件可选性(optional)

前面说的这个模式,对appsettings.json文件是必选的,如果不存在就会报错。

// 添加Json文件数据源
builder.AddJsonFile("appsettings2.json");

就会报错。

image

这里可以让文件可选。

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Json文件数据源
    builder.AddJsonFile("appsettings2.json", optional: true);
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    // 获取并输出配置根下的值
    Console.WriteLine(configurationRoot["car1"]);
    Console.WriteLine(configurationRoot["car2"]);
    Console.WriteLine(configurationRoot["car3"]);
    Console.WriteLine(configurationRoot["car4"]);
    Console.WriteLine(configurationRoot["car5"]);
}

这次就不报错了,这里将可选的第二个参数optional设置成了true

设置文件变更通知(reloadOnChange)

对于AddJsonFile方法而言,reloadOnChange默认值就是false,这里我们把它改成True,也就意味着文件变更后是会重新生效的的。

public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path)
{
    return AddJsonFile(builder, provider: null, path: path, optional: false, reloadOnChange: false);
}
static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Json文件数据源
    builder.AddJsonFile("appsettings.json", optional:true, reloadOnChange: true);
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    // 获取并输出配置根下的值
    Console.WriteLine(configurationRoot["car1"]);
    Console.WriteLine(configurationRoot["car2"]);
    Console.WriteLine(configurationRoot["car3"]);
    Console.WriteLine(configurationRoot["car4"]);
    Console.WriteLine(configurationRoot["car5"]);
    Console.ReadKey();

    // 获取并输出配置根下的值
    Console.WriteLine(configurationRoot["car1"]);
    Console.WriteLine(configurationRoot["car2"]);
    Console.WriteLine(configurationRoot["car3"]);
    Console.WriteLine(configurationRoot["car4"]);
    Console.WriteLine(configurationRoot["car5"]);
    Console.ReadKey();
}

这里输出两次,进行第二次之前,我们去修改下bin下面的appsettings.json文件,我们会发现,这时候两次的值是不一样的,也就是第二次输出了我们改变后的值。

image

使用Ini文件配置(AddIniFile)

依赖包

dotnet add package Microsoft.Extensions.Configuration.Ini

添加Ini配置文件(appsettings.ini)

tesla=Model Y

示例代码

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Ini文件数据源
    builder.AddIniFile("appsettings.ini");
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    // 获取并输出配置根下的值
    Console.WriteLine(configurationRoot["tesla"]);
    Console.ReadKey();
}
public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path)
{
    return AddIniFile(builder, provider: null, path: path, optional: false, reloadOnChange: false);
}

配置文件的顺序

后添加的Key会覆盖前面的。

当我们同时添加了两个配置文件,如果他们有重复的key,那么后添加的会覆盖前面的。

appsettings.json

{
  "car1": "tesla",
  "car2": "byd",
  "car3": "baoma",
  "car4": 20,
  "car5": false
}

appsettings.dev.json

{
  "car1": "tesla",
  "car2": "byd",
  "car3": "baoma",
  "car4": 100,
  "car5": false
}
internal class Program
{
    static void Main(string[] args)
    {
        // 创建配置构建器
        IConfigurationBuilder builder = new ConfigurationBuilder();
        // 添加Json文件数据源
        builder.AddJsonFile("appsettings.json");
        // 添加Json文件数据源
        builder.AddJsonFile("appsettings.dev.json");
        // 构建配置获取配置根
        IConfigurationRoot configurationRoot = builder.Build();
        // 获取并输出配置根下的值
        Console.WriteLine(configurationRoot["car1"]);
        Console.WriteLine(configurationRoot["car2"]);
        Console.WriteLine(configurationRoot["car3"]);
        Console.WriteLine(configurationRoot["car4"]);
        Console.WriteLine(configurationRoot["car5"]);
        Console.ReadKey();
    }
}

这时候我们看到结果是

image

配置热更新能力

场景

  • 需要追踪数据源的变更时
  • 需要在配置数据变更时触发特定操作时

关键方法

IChangeToken IConfiguration.GetReloadToken()

其中IChangeToken的定义是

namespace Microsoft.Extensions.Primitives
{
    public interface IChangeToken
    {
        bool HasChanged { get; }

        bool ActiveChangeCallbacks { get; }

        IDisposable RegisterChangeCallback(Action<object> callback, object state);
    }
}

通过IChangeToken来响应配置变更

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Json文件数据源
    builder.AddJsonFile("appsettings.json",optional: false,reloadOnChange: true);
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    IChangeToken token = configurationRoot.GetReloadToken();
    token.RegisterChangeCallback(state =>
    {
        // 获取并输出配置根下的值
        Console.WriteLine(configurationRoot["car1"]);
        Console.WriteLine(configurationRoot["car2"]);
        Console.WriteLine(configurationRoot["car3"]);
        Console.WriteLine(configurationRoot["car4"]);
        Console.WriteLine(configurationRoot["car5"]);
    }, configurationRoot);
    Console.ReadKey();
}

刚启动没动静,这时候我们去编辑下配置文件,正常输出了。

image

通过ChangeToken.OnChange来处理配置变更监听

但是如果再编辑,没反应了,因为IChangeToken只能使用一次,如果还需要监听下一个,不得不循环写。

实际上,微软给我们提供了一个更加方便的方法ChangeToken.OnChange()来处理这种情况。

public static class ChangeToken
{
    public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
    {
        if (changeTokenProducer == null)
        {
            throw new ArgumentNullException(nameof(changeTokenProducer));
        }
        if (changeTokenConsumer == null)
        {
            throw new ArgumentNullException(nameof(changeTokenConsumer));
        }

        return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
    }

我们看到,它的第一个参数是Func方式的委托方法,我们需要把如何获取IChangeToken的实现方法传进来,第二个参数则是发现配置变更需要执行的Action方式的委托方法。

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Json文件数据源
    builder.AddJsonFile("appsettings.json",optional: false,reloadOnChange: true);
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    ChangeToken.OnChange(()=> configurationRoot.GetReloadToken(), () =>
    {
        // 获取并输出配置根下的值
        Console.WriteLine(configurationRoot["car1"]);
        Console.WriteLine(configurationRoot["car2"]);
        Console.WriteLine(configurationRoot["car3"]);
        Console.WriteLine(configurationRoot["car4"]);
        Console.WriteLine(configurationRoot["car5"]);
    });
    Console.ReadKey();
}

这样顺带也解决了上面方法需要手写循环监听代码的问题。

image

强类型接收配置

要点

  • 支持将配置值绑定到已有的对象上
  • 支持将配置值绑定到私有属性上

使用强类型接收配置

依赖包

dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder

定义一个强类型类CustomConfig

internal class CustomConfig
{
    public string Car1 { get; set; }

    public string Car2 { get; set; }

    public string Car3 { get; set; }

    public int Car4 { get; set; }

    public bool Car5 { get; set; }
}

然后通过IConfigurationRoot的扩展方法Bind来实现强类型和配置的绑定。

namespace Microsoft.Extensions.Configuration
{
    public static class ConfigurationBinder
    {
        [RequiresUnreferencedCode(InstanceGetTypeTrimmingWarningMessage)]
        public static void Bind(this IConfiguration configuration, object instance)
            => configuration.Bind(instance, o => { });
    }
}
static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Json文件数据源
    builder.AddJsonFile("appsettings.json",optional: false,reloadOnChange: true);
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    var customConfig = new CustomConfig
    {
        Car1 = "xxx",
        Car2 = "xxx",
        Car3 = "xxx",
        Car4 = -1,
        Car5 = true
    };
    configurationRoot.Bind(customConfig);

    // 获取并输出配置根下的值
    Console.WriteLine(customConfig.Car1);
    Console.WriteLine(customConfig.Car2);
    Console.WriteLine(customConfig.Car3);
    Console.WriteLine(customConfig.Car4);
    Console.WriteLine(customConfig.Car5);
    Console.ReadKey();
}

运行结果发现,所有的值都对象类的默认值不一样了,得到就是配置文件的值

image

配置分组绑定

appsetting.json改成更加复杂的结构

{
  "car1": "tesla",
  "car2": "byd",
  "car3": "baoma",
  "car4": 20,
  "car5": false,
  "model": {
    "car1": "y",
    "car2": "x",
    "car3": "s",
    "car4": 3,
    "car5": true
  }
}

然后我们从model这个section来做绑定

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    // 添加Json文件数据源
    builder.AddJsonFile("appsettings.json",optional: false,reloadOnChange: true);
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    var customConfig = new CustomConfig
    {
        Car1 = "xxx",
        Car2 = "xxx",
        Car3 = "xxx",
        Car4 = -1,
        Car5 = true
    };
    configurationRoot.GetSection("model").Bind(customConfig);

    // 获取并输出配置根下的值
    Console.WriteLine(customConfig.Car1);
    Console.WriteLine(customConfig.Car2);
    Console.WriteLine(customConfig.Car3);
    Console.WriteLine(customConfig.Car4);
    Console.WriteLine(customConfig.Car5);
    Console.ReadKey();
}

执行结果

image

绑定包含私有对象

如果CustomConfig中存在私有字段

internal class CustomConfig
{
    public string Car1 { get; set; }

    public string Car2 { get; set; }

    public string Car3 { get; set; }

    public int Car4 { get; set; }

    public bool Car5 { get; private set; }
}

这时候直接绑定是得不到我们想要的Car5的值的,但是我们可以通过Bind的可选参数BinderOptions.BindNonPublicProperties来配置它。

[RequiresUnreferencedCode(InstanceGetTypeTrimmingWarningMessage)]
public static void Bind(this IConfiguration configuration, object instance, Action<BinderOptions> configureOptions)
{
    if (configuration == null)
    {
        throw new ArgumentNullException(nameof(configuration));
    }

    if (instance != null)
    {
        var options = new BinderOptions();
        configureOptions?.Invoke(options);
        BindInstance(instance.GetType(), instance, configuration, options);
    }
}
/// <summary>
/// Options class used by the <see cref="ConfigurationBinder"/>.
/// </summary>
public class BinderOptions
{
    /// <summary>
    /// When false (the default), the binder will only attempt to set public properties.
    /// If true, the binder will attempt to set all non read-only properties.
    /// </summary>
    public bool BindNonPublicProperties { get; set; }

    /// <summary>
    /// When false (the default), no exceptions are thrown when a configuration key is found for which the
    /// provided model object does not have an appropriate property which matches the key's name.
    /// When true, an <see cref="System.InvalidOperationException"/> is thrown with a description
    /// of the missing properties.
    /// </summary>
    public bool ErrorOnUnknownConfiguration { get; set; }
}
configurationRoot.GetSection("model").Bind(customConfig,
                configureOptions => { configureOptions.BindNonPublicProperties = true; });

这样配置之后,也就可以正常绑定了。

自定义配置数据源

扩展步骤

  • 实现IConfigurationSource
  • 实现IConfigurationProvider
  • 实现AddExtensionName扩展方法

自定义IConfigurationSource

依赖包

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Abstractions

自定义CustomConfigurationSource继承自IConfigurationSource接口,必须实现Build方法。

class CustomConfigurationSource : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new CustomConfigurationProvider();
    }
}

自定义ConfigurationProvider

这里存在一个已经继承了IConfigurationProvider的虚方法ConfigurationProvider,我们直接继承它。

public abstract class ConfigurationProvider : IConfigurationProvider
{
    private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();

    /// <summary>
    /// Initializes a new <see cref="IConfigurationProvider"/>
    /// </summary>
    protected ConfigurationProvider()
    {
        Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    }
class CustomConfigurationProvider : ConfigurationProvider
{
    readonly Timer timer;

    public CustomConfigurationProvider(): base()
    {
        timer = new Timer();
        timer.Elapsed += Timer_Elapsed;
        timer.Interval = 3000;
        timer.Start();
    }

    private void Timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        // 加载数据
        Load(true);
    }

    public override void Load()
    {
        // 加载数据
        Load(false);
    }

    void Load(bool reload)
    {
        this.Data["lastTime"] = DateTime.Now.ToString();
        if (reload)
        {
            base.OnReload();
        }
    }
}

这里重写了Load方法,并且采用一个定时器来模拟3秒钟就重新加载一次配置的动作。

使用自定义配置源

直接通过IConfigurationBuilderAdd方法就可以把我们自定义的配置源加进来。

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.Add(new CustomConfigurationSource());
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    ChangeToken.OnChange(() => configurationRoot.GetReloadToken(), () =>
    {
        // 获取并输出配置根下的值
        Console.WriteLine(configurationRoot["lastTime"]);
    });
    Console.ReadKey();
}

这里我们采用监听的方式输出下我们添加一个配置,输入结果

image

通过扩展方法来封装自定义数据源

这里我们新建一个.Net Standard的类库项目来把上面的Source和Provider封装起来。

image

接下来,我们新建一个全局的扩展类CustomConfigurationBuilderExtensions来实现前面的添加步骤。

public static class CustomConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddCustomConfiguration(this IConfigurationBuilder builder)
    {
        builder.Add(new CustomConfigurationSource());
        return builder;
    }
}

Program.cs中使用的时候只需要变化为builder.AddCustomConfiguration()即可。

static void Main(string[] args)
{
    // 创建配置构建器
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddCustomConfiguration();
    // 构建配置获取配置根
    IConfigurationRoot configurationRoot = builder.Build();
    ChangeToken.OnChange(() => configurationRoot.GetReloadToken(), () =>
    {
        // 获取并输出配置根下的值
        Console.WriteLine(configurationRoot["lastTime"]);
    });
    Console.ReadKey();
}

输出结果如下

image

其实平时我们看到的很多用法都是采用扩展方法来实现的。

services.AddControllers();

image

app.UseHttpsRedirection();

image

这样做的好处是可以将对应的逻辑实现更好的封装,让使用者更加简洁。

使用选项框架处理服务和配置关系

选项框架特性

  • 支持单例模式读取配置
  • 支持快照
  • 支持配置变更通知
  • 支持运行时动态修改选项值

遵循设计原则

  • 接口分离原则(ISP),类不应该依赖它不适用的配置
  • 关注点分离(SoC),不同组件、服务、类之间的配置不应该互相依赖或耦合

实现建议

  • 为服务设计XXXOptions
  • 使用IOptions<XXXOptions>IOptionsSnapshot<XXXOptions>IOptionsMonitor<XXXOptions>作为服务构造函数的参数

建立项目和示例代码

dotnet new webapi -o demoForOptions31 -f netcoreapp3.1
dotnet sln add .\demoForOptions31\demoForOptions31.csproj

image

准备订单服务和订单服务选项配置代码

public interface IOrderService
{
    int ShowMaxOrderCount();
}

public class OrderService : IOrderService
{
    readonly OrderServiceOptions _options;

    public OrderService(OrderServiceOptions options)
    {
        this._options = options;
    }

    public int ShowMaxOrderCount()
    {
        return _options.MaxOrderCount;
    }
}

public class OrderServiceOptions
{
    public int MaxOrderCount { get; set; } = 100;
}

Startup.csConfigureServices方法中注册服务和服务配置

services.AddSingleton<OrderServiceOptions>();
services.AddSingleton<IOrderService, OrderService>();

WeatherForecastController.cs添加输出服务配置结果的代码

[HttpGet]
public int Get([FromServices]IOrderService orderService)
{
    Console.WriteLine($"Max Order Count:{orderService.ShowMaxOrderCount()}");
    return 1;
}

启动后顺利输入服务配置的默认值

image

将选项框架和配置绑定起来

依赖包

Microsoft.Extensions.Options

不过在Microsoft.AspNetCore.App框架中已经把它包含进来了。

image

使用IOptions把我们定义的OrderServiceOptions包裹起来。

public interface IOrderService
{
    int ShowMaxOrderCount();
}

public class OrderService : IOrderService
{
    readonly IOptions<OrderServiceOptions> _options;

    public OrderService(IOptions<OrderServiceOptions> options)
    {
        this._options = options;
    }

    public int ShowMaxOrderCount()
    {
        return _options.Value.MaxOrderCount;
    }
}

public class OrderServiceOptions
{
    public int MaxOrderCount { get; set; } = 100;
}

我们在配置文件appsettings.json里面添加配置节点

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "OrderService": {
    "MaxOrderCount": 200
  }
}

Startup.csConfigureServices方法中将配置和选项框架绑定起来

services.Configure<OrderServiceOptions>(Configuration.GetSection("OrderService"));
services.AddSingleton<IOrderService, OrderService>();

最终运行可以看到,输出结果是配置中的200值了

image

在上面的例子中我们看到,我们的服务只依赖了服务对应的Options,并没有依赖配置框架,服务只关心配置的值是什么,它并不关心配置的值是从何而来,成功解除了配置和服务之间的依赖;我们还可以为不同服务设计它的Options,不同服务之间不存在依赖。

选项框架处理配置热更新

  • 范围作用域类型使用IOptionsSnapshot
  • 单例服务使用IOptionsMonitor

在范围作用域注入服务的场景下,我们需要可以将IOptions改成IOptionsSnapshot

services.Configure<OrderServiceOptions>(Configuration.GetSection("OrderService"));
services.AddScoped<IOrderService, OrderService>();
public class OrderService : IOrderService
{
    readonly IOptionsSnapshot<OrderServiceOptions> _options;

    public OrderService(IOptionsSnapshot<OrderServiceOptions> options)
    {
        this._options = options;
    }

    public int ShowMaxOrderCount()
    {
        return _options.Value.MaxOrderCount;
    }
}

这时候每次请求得到的配置输出值就是配置文件最新的。

在单例模式注入服务的场景下,我们需要可以将IOptions改成IOptionsMonitor

services.Configure<OrderServiceOptions>(Configuration.GetSection("OrderService"));
services.AddSingleton<IOrderService, OrderService>();
public class OrderService : IOrderService
{
    readonly IOptionsMonitor<OrderServiceOptions> _options;

    public OrderService(IOptionsMonitor<OrderServiceOptions> options)
    {
        this._options = options;
    }

    public int ShowMaxOrderCount()
    {
        return _options.CurrentValue.MaxOrderCount;
    }
}

这里注意下,_options.CurrentValue有所修改。

这时候每次请求得到的配置输出值就是配置文件最新的。

这里我们还可以监听到配置发生变化,使用IOptionsMonitorOnChange方法即可

public OrderService(IOptionsMonitor<OrderServiceOptions> options)
{
    this._options = options;

    options.OnChange((options) =>
    {
        Console.WriteLine($"Last Max Order Count:{options.MaxOrderCount}");
    });
}

查看输出

image

通过静态扩展优化代码

定义一个服务扩展静态类

public static class OrderServiceExtensions
{
    public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<OrderServiceOptions>(configuration);
        services.AddSingleton<IOrderService, OrderService>();
        return services;
    }
}

这里设计从外面把配置传进来完成选项框架的绑定。

Startup.csConfigureServices方法中只需要调用这个静态扩展方法即可

services.AddOrderService(Configuration.GetSection("OrderService"));
services.AddControllers();

通过代码更新动态配置的值

  • IPostConfigureOption<TOptions>

把配置读取出来之后,还需要一些特殊处理,我们可以在选项框架和配置绑定之后,我们可以使用PostConfigure来处理。

public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
{
    services.Configure<OrderServiceOptions>(configuration);
    services.PostConfigure<OrderServiceOptions>(options =>
    {
        options.MaxOrderCount += 1000;
    });

    services.AddSingleton<IOrderService, OrderService>();
    return services;
}

输出结果能达到预期

image

汇总选项框架扩展方法

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection AddOptions(this IServiceCollection services);

    public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services) where TOptions : class;

    public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name) where TOptions : class;

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class;

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class;

    public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class;

    public static IServiceCollection ConfigureOptions(this IServiceCollection services, object configureInstance);

    public static IServiceCollection ConfigureOptions(this IServiceCollection services, Type configureType);

    public static IServiceCollection ConfigureOptions<TConfigureOptions>(this IServiceCollection services) where TConfigureOptions : class;

    public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class;

    public static IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class;
}

给选择数据添加验证逻辑

场景

如果配置错误了,流量可以请求到配置错误的服务上,我们可以给配置添加验证逻辑,阻止错误配置的服务启动。

三种验证方法

  • 直接注册验证函数
  • 实现IValidateOptions<TOptions>
  • 使用Microsoft.Extensions.Options.DataAnotations

添加选项的验证函数

这里将前面的Configure<TOptions>改成AddOptions并且实现配置绑定,然后在后面追加验证逻辑

public static class OrderServiceExtensions
{
    public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddOptions<OrderServiceOptions>().Configure(options => {
            configuration.Bind(options);
        }).Validate(options =>
        {
            return options.MaxOrderCount <= 100;
        }, "Max Order Count 不能大于100");

        services.AddSingleton<IOrderService, OrderService>();
        return services;
    }
}

这时候如果配置不满足条件就会报错

image

使用属性输入的方式来添加验证

public static class OrderServiceExtensions
{
    public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddOptions<OrderServiceOptions>().Configure(options =>
        {
            configuration.Bind(options);
        }).ValidateDataAnnotations();

        services.AddSingleton<IOrderService, OrderService>();
        return services;
    }
}

然后就可以在OrderServiceOptions做属性的验证策略

public class OrderServiceOptions
{
    [Range(0,10)]
    public int MaxOrderCount { get; set; } = 100;
}

这时候运行可以看到报错了

image

通过实现接口来添加验证

定义对服务选项的IValidateOptions的实现

public class OrderServiceValidateOptions : IValidateOptions<OrderServiceOptions>
{
    public ValidateOptionsResult Validate(string name, OrderServiceOptions options)
    {
        if(options.MaxOrderCount > 100)
        {
            return ValidateOptionsResult.Fail("Max Order Count 不能大于100");
        }
        else
        {
            return ValidateOptionsResult.Success;
        }
    }
}

然后在把它注入进来

public static class OrderServiceExtensions
{
    public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddOptions<OrderServiceOptions>().Configure(options =>
        {
            configuration.Bind(options);
        }).Services.AddSingleton<IValidateOptions<OrderServiceOptions>, OrderServiceValidateOptions>();

        services.AddSingleton<IOrderService, OrderService>();
        return services;
    }
}

这时候运行,发现也是可以进行验证的。

image

集成分布式配置中心Apollo

配置信息

appsettings.json里面我们需要配置apollo的配置节点信息,其中一个重要的是appId,它来自配置中心的项目名称

image

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "apollo": {
    "AppId": "SampleApp",
    "Env": "DEV"
  },
  "AllowedHosts": "*"
}

另外还有个Env代表配置的环境,它有如下选项:

数值 含义
DEV 开发环境(Development environment)
FAT 测试环境(Feature Acceptance Test environment)
UAT 验证环境(User Acceptance Test environment)
PRO 生产环境(Production environment)

从配置项目的右侧环境列表中可以找到已经配置哪些环境的配置

image

不过一般来说,不同环境的Apollo通常我们会单独部署,也就是不会存在一套Apollo对应多个环境的情况,这时候我们只需要在服务地址上区分就好了

appsettings.Development.json配置而言,直接配置为:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "apollo": {
    "AppId": "SampleApp",
    "MetaServer": "http://localhost:8080"
  }
}

本地缓存路径

Apollo客户端会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。

本地缓存路径位于C:\opt\data\{appId}\config-cache,所以请确保C:\opt\data\目录存在,且应用有读写权限。

可指定配置集群

Apollo支持配置按照集群划分,也就是说对于一个appId和一个环境,对不同的集群可以有不同的配置。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "apollo": {
    "AppId": "SampleApp",
    "MetaServer": "http://localhost:8080",
    "Cluster": "SomeCluster"
  }
}

指定接收的Namespaces

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "apollo": {
    "AppId": "SampleApp",
    "MetaServer": "http://localhost:8080",
    "Namespaces": [ "some namespace", "application.json", "application" ]
  }
}

代码集成Apollo配置

依赖包

https://www.nuget.org/packages/Com.Ctrip.Framework.Apollo.Configuration

dotnet add package Com.Ctrip.Framework.Apollo.Configuration

image

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "apollo": {
    "AppId": "SampleApp",
    "MetaServer": "http://localhost:8080"
  },
  "AllowedHosts": "*"
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration(builder => builder
        .AddApollo(builder.Build().GetSection("apollo")))
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

参考

posted @ 2022-09-17 00:34  TaylorShi  阅读(122)  评论(0编辑  收藏  举报