乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core配置框架,让服务无缝适应各种环境
ASP.NET Core配置框架
ASP.NET Core配置框架的核心组件包是如下两个:
- Microsoft.Extensions.Configuration.Abstractions,配置框架抽象定义
- Microsoft.Extensions.Configuration,配置框架默认实现。
获取安装配置框架包
https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Abstractions
dotnet add package Microsoft.Extensions.Configuration.Abstractions
https://www.nuget.org/packages/Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration
框架特点
- 以Key-Value字符串的键值对方式抽象了配置。
- 支持从各种不同的数据源读取配置。
核心类型
- IConfiguration
- IConfigurationRoot
- IConfigurationSection
- IConfigurationBuilder
框架扩展点
- IConfigurationSource
- IConfigurationProvider
实践理解
创建项目安装Nuget包
使用内存数据源(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
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官方的示例
这里的-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的一些内置特殊配置时。
特性
- 对于配置的分层键,支持用双下横线
__
替代:
- 支持根据前缀加载
配置调试启动环境变量
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");
就会报错。
这里可以让文件可选。
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
文件,我们会发现,这时候两次的值是不一样的,也就是第二次输出了我们改变后的值。
使用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();
}
}
这时候我们看到结果是
配置热更新能力
场景
- 需要追踪数据源的变更时
- 需要在配置数据变更时触发特定操作时
关键方法
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();
}
刚启动没动静,这时候我们去编辑下配置文件,正常输出了。
通过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();
}
这样顺带也解决了上面方法需要手写循环监听代码的问题。
强类型接收配置
要点
- 支持将配置值绑定到已有的对象上
- 支持将配置值绑定到私有属性上
使用强类型接收配置
依赖包
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();
}
运行结果发现,所有的值都对象类的默认值不一样了,得到就是配置文件的值
配置分组绑定
将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();
}
执行结果
绑定包含私有对象
如果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秒钟就重新加载一次配置的动作。
使用自定义配置源
直接通过IConfigurationBuilder
的Add
方法就可以把我们自定义的配置源加进来。
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();
}
这里我们采用监听的方式输出下我们添加一个配置,输入结果
通过扩展方法来封装自定义数据源
这里我们新建一个.Net Standard的类库项目来把上面的Source和Provider封装起来。
接下来,我们新建一个全局的扩展类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();
}
输出结果如下
其实平时我们看到的很多用法都是采用扩展方法来实现的。
services.AddControllers();
app.UseHttpsRedirection();
这样做的好处是可以将对应的逻辑实现更好的封装,让使用者更加简洁。
使用选项框架处理服务和配置关系
选项框架特性
- 支持单例模式读取配置
- 支持快照
- 支持配置变更通知
- 支持运行时动态修改选项值
遵循设计原则
- 接口分离原则(ISP),类不应该依赖它不适用的配置
- 关注点分离(SoC),不同组件、服务、类之间的配置不应该互相依赖或耦合
实现建议
- 为服务设计
XXXOptions
- 使用
IOptions<XXXOptions>
、IOptionsSnapshot<XXXOptions>
、IOptionsMonitor<XXXOptions>
作为服务构造函数的参数
建立项目和示例代码
dotnet new webapi -o demoForOptions31 -f netcoreapp3.1
dotnet sln add .\demoForOptions31\demoForOptions31.csproj
准备订单服务和订单服务选项配置代码
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.cs
的ConfigureServices
方法中注册服务和服务配置
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;
}
启动后顺利输入服务配置的默认值
将选项框架和配置绑定起来
依赖包
Microsoft.Extensions.Options
不过在Microsoft.AspNetCore.App
框架中已经把它包含进来了。
使用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.cs
的ConfigureServices
方法中将配置和选项框架绑定起来
services.Configure<OrderServiceOptions>(Configuration.GetSection("OrderService"));
services.AddSingleton<IOrderService, OrderService>();
最终运行可以看到,输出结果是配置中的200值了
在上面的例子中我们看到,我们的服务只依赖了服务对应的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
有所修改。
这时候每次请求得到的配置输出值就是配置文件最新的。
这里我们还可以监听到配置发生变化,使用IOptionsMonitor
的OnChange
方法即可
public OrderService(IOptionsMonitor<OrderServiceOptions> options)
{
this._options = options;
options.OnChange((options) =>
{
Console.WriteLine($"Last Max Order Count:{options.MaxOrderCount}");
});
}
查看输出
通过静态扩展优化代码
定义一个服务扩展静态类
public static class OrderServiceExtensions
{
public static IServiceCollection AddOrderService(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<OrderServiceOptions>(configuration);
services.AddSingleton<IOrderService, OrderService>();
return services;
}
}
这里设计从外面把配置传进来完成选项框架的绑定。
在Startup.cs
的ConfigureServices
方法中只需要调用这个静态扩展方法即可
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;
}
输出结果能达到预期
汇总选项框架扩展方法
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;
}
}
这时候如果配置不满足条件就会报错
使用属性输入的方式来添加验证
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;
}
这时候运行可以看到报错了
通过实现接口来添加验证
定义对服务选项的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;
}
}
这时候运行,发现也是可以进行验证的。
集成分布式配置中心Apollo
配置信息
在appsettings.json
里面我们需要配置apollo
的配置节点信息,其中一个重要的是appId
,它来自配置中心的项目名称
{
"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) |
从配置项目的右侧环境列表中可以找到已经配置哪些环境的配置
不过一般来说,不同环境的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
{
"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>();
});
参考
- ASP.NET Core 配置 EF 框架服务
- .NET Core 的配置框架
- ASP .NET Core配置框架
- Microsoft.Extensions.Configuration Namespace
- 抽丝剥茧读源码——Microsoft.Extensions.Configuration
- JSON-handle
- 乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 官方扩展集锦(Microsoft.Extensions on Nuget)
- Apollo.net框架集成
- https://github.com/apolloconfig/apollo.net
- .NET Core微服务之基于Apollo实现统一配置中心
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步