.NET Core开发实战课程备忘(10) -- 选项框架:服务组件集成配置的最佳实现

选项框架特性

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

设计原则

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

建议

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

代码实现

创建名为OptionsDemoASP.NET Core项目,类型为API

创建测试服务与测试服务对应的选项

创建OrderService.cs为了方便测试,这里将IOrderServiceOrderServiceOrderServiceOption都放在OrderService.cs文件中,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using Microsoft.Extensions.Options;

namespace OptionsDemo.Services
{
public interface IOrderService
{
int ShowMaxOrderCount();
}

public class OrderService:IOrderService
{

private readonly IOptions<OrderServiceOptions> _options;
public OrderService(IOptions<OrderServiceOptions> options)
{
_options = options;
}
public int ShowMaxOrderCount()
{
return _options.Value.MaxOrderCount;
}
}

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

修改appsettings.json

将项目中的appsettings.json的内容修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"OrderService": {
"MaxOrderCount": 400
}
}

注册服务和选项

Startup.ConfigureServices中添加以下代码:

1
2
services.Configure<OrderServiceOptions>(Configuration.GetSection("OrderService"));
services.AddScoped<IOrderService, OrderService>();

获取服务进行测试

修改WeatherForecastController.Get方法,具体代码如下:

1
2
3
4
5
6
[HttpGet]
public int Get([FromServices]IOrderService orderService)
{
Console.WriteLine($"orderService.ShowMaxOrderCount:{orderService.ShowMaxOrderCount()}");
return 1;
}

运行项目,可以访问/WeatherForecast,可以看到控制台打印出以下信息:

1
orderService.ShowMaxOrderCount:400

这里如果在项目运行时修改appsettings.json里配置项的值,重新访问/WeatherForecast地址,会发现控制台打印出来的值不会变,还依旧是400,这里就需要使用到下面的热更新。

选项框架热更新

关键类型

  • 单例服务(Singleton)使用IOptionsMonitor<out TOptions>
  • 范围作用域类型(Scope)使用IOptionsSnapshot<out TOptions>

代码示例

IOptionsSnapshot

上个示例对OrderService注册的是Scope服务,所以这里先测试Scope对应的IOptionsSnapshot,将OrderService构造函数获取服务的类型修改为IOptionsSnapshot<OrderServiceOptions>,最终修改后OrderService类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderService:IOrderService
{
private readonly IOptionsSnapshot<OrderServiceOptions> _options;
public OrderService(IOptionsSnapshot<OrderServiceOptions> options)
{
_options = options;
}
public int ShowMaxOrderCount()
{
return _options.Value.MaxOrderCount;
}
}

运行代码,访问/WeatherForecast,发现现在打印出来的是appsettings.json现有的值,对该配置项进行修改,保存之后重新访问/WeatherForecast,可以发现获取到的是新的值

IOptionsMonitor

OrderService构造函数获取服务的类型修改为IOptionsMonitor<OrderServiceOptions>,最终修改后OrderService类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class OrderService:IOrderService
{
private readonly IOptionsMonitor<OrderServiceOptions> _options;
public OrderService(IOptionsMonitor<OrderServiceOptions> options)
{
_options = options;
}
public int ShowMaxOrderCount()
{
return _options.CurrentValue.MaxOrderCount;
}
}

Startup.ConfigureServices方法中将OrderService注册为单例模式,代码如下:

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

运行代码,访问/WeatherForecast,发现现在打印出来的是appsettings.json现有的值,对该配置项进行修改,保存之后重新访问/WeatherForecast,可以发现获取到的是新的值

IOptionsMonitor监听配置变动

通过IOptionsMonitor对象的OnChange方法来注册配置变动操作,只需要在获取对象后注册相应操作即可,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OrderService:IOrderService
{
private readonly IOptionsMonitor<OrderServiceOptions> _options;
public OrderService(IOptionsMonitor<OrderServiceOptions> options)
{
_options = options;
this._options.OnChange(changedOptions =>
{
Console.WriteLine($"配置发生了变化,新值为:{changedOptions.MaxOrderCount}");
});
}
public int ShowMaxOrderCount()
{
return _options.CurrentValue.MaxOrderCount;
}
}

运行代码,修改appsettings.json的值,就可以看到控制台打印出类似以下信息:

1
配置发生了变化,新值为:100

优化代码结构

在开发中经常会出现服务与选项一起注册的情况,为了优化代码结构,一般会为统一将一个服务的注册放到IServiceCollection的扩展方法中去。创建OrderServiceExtensions.cs,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

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

Startup.ConfigureServices修改后代码如下:

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
services.AddOrderService(Configuration);
services.AddControllers();
}

运行之后可发现效果与上面一致

动态修改选项值

在注入选项之后,可动态对选项的值进行操作,这里以为MaxOrderCount的值增加100为例,修改OrderServiceExtensions,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace OptionsDemo.Services
{
public static class OrderServiceExtensions
{
public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<OrderServiceOptions>(configuration.GetSection("OrderService"));
services.PostConfigure<OrderServiceOptions>(options =>
{
options.MaxOrderCount += 100;
});
services.AddSingleton<IOrderService, OrderService>();
return services;
}
}
}

运行代码,可发现获得到的值比appsettings.json里的值增加100