29 BINDING CONFIGURATION AND OPTIONS PATTERN
29 绑定配置和选项模式
In the previous chapter, we had to use our appsettings file to store some important values for our JWT configuration and read those values from it:
在上一章中,我们必须使用 appsettings 文件来存储 JWT 配置的一些重要值,并从中读取这些值:
"JwtSettings": { "validIssuer": "CodeMazeAPI", "validAudience": "https://localhost:5001", "expires": 5 },
To access these values, we’ve used the GetSection method from the IConfiguration interface:
为了访问这些值,我们使用了 IConfiguration 接口中的 GetSection 方法:
var jwtSettings = configuration.GetSection("JwtSettings");
The GetSection method gets a sub-section from the appsettings file based on the provided key.
GetSection 方法根据提供的键从 appsettings 文件中获取子部分。
Once we extracted the sub-section, we’ve accessed the specific values by using the jwtSettings variable of type IConfigurationSection, with the key provided inside the square brackets:
提取子部分后,我们使用 IConfigurationSection 类型的 jwtSettings 变量访问了特定值,其中键在方括号内提供:
ValidIssuer = jwtSettings["validIssuer"],
This works great but it does have its flaws.
这效果很好,但它也有其缺陷。
Having to type sections and keys to get the values can be repetitive and error-prone. We risk introducing errors to our code, and these kinds of errors can cost us a lot of time until we discover them since someone else can introduce them, and we won’t notice them since a null result is returned when values are missing.
必须键入 sections 和 keys 才能获取值可能是重复且容易出错的。我们冒着将错误引入代码的风险,这些类型的错误可能会花费我们大量时间,直到我们发现它们,因为其他人可能会引入它们,而且我们不会注意到它们,因为当值缺失时会返回 null 结果。
To overcome this problem, we can bind the configuration data to strongly typed objects. To do that, we can use the Bind method.
为了解决这个问题,我们可以将配置数据绑定到强类型对象。为此,我们可以使用 Bind 方法。
29.1 Binding Configuration
29.1 绑定配置
To start with the binding process, we are going to create a new ConfigurationModels folder inside the Entities project, and a new JwtConfiguration class inside that folder:
要开始绑定过程,我们将在 Entities 项目中创建一个新的 ConfigurationModels 文件夹,并在该文件夹中创建一个新的 JwtConfiguration 类:
public class JwtConfiguration { public string Section { get; set; } = "JwtSettings"; public string? ValidIssuer { get; set; } public string? ValidAudience { get; set; } public string? Expires { get; set; } }
Then in the ServiceExtensions class, we are going to modify the ConfigureJWT method:
然后在 ServiceExtensions 类中,我们将修改 ConfigureJWT 方法:
public static void ConfigureJWT(this IServiceCollection services, IConfiguration configuration) { var jwtConfiguration = new JwtConfiguration(); configuration.Bind(jwtConfiguration.Section, jwtConfiguration); var secretKey = Environment.GetEnvironmentVariable("SECRET"); services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtConfiguration.ValidIssuer, ValidAudience = jwtConfiguration.ValidAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) }; }); }
We create a new instance of the JwtConfiguration class and use the Bind method that accepts the section name and the instance object as parameters, to bind to the JwtSettings section directly and map configuration values to respective properties inside the JwtConfiguration class. Then, we just use those properties instead of string keys inside square brackets, to access required values.
我们创建 JwtConfiguration 类的新实例,并使用接受节名称和实例对象作为参数的 Bind 方法,直接绑定到 JwtSettings 节,并将配置值映射到 JwtConfiguration 类中的相应属性。然后,我们只使用这些属性而不是方括号内的字符串键来访问所需的值。
There are two things to note here though. The first is that the names of the configuration data keys and class properties must match. The other is that if you extend the configuration, you need to extend the class as well, which can be a bit cumbersome, but it beats getting values by typing strings.
不过,这里有两件事需要注意。首先是配置数据键和类属性的名称必须匹配。另一个是,如果你扩展配置,你也需要扩展 class,这可能有点麻烦,但它比通过键入字符串来获取值要好。
Now, we can continue with the AuthenticationService class modification since we extract configuration values in two methods from this class:
现在,我们可以继续修改 AuthenticationService 类,因为我们从这个类中提取了两个方法的配置值:
... private readonly JwtConfiguration _jwtConfiguration; private User? _user; public AuthenticationService(ILoggerManager logger, IMapper mapper, UserManager<User> userManager, IConfiguration configuration) { _logger = logger; _mapper = mapper; _userManager = userManager; _configuration = configuration; _jwtConfiguration = new JwtConfiguration(); _configuration.Bind(_jwtConfiguration.Section, _jwtConfiguration); }
So, we add a readonly variable, and create an instance and execute binding inside the constructor.
因此,我们添加一个 readonly 变量,并在构造函数中创建一个实例并执行绑定。
And since we’re using the Bind() method we need to install the Microsoft.Extensions.Configuration.Binder NuGet package.
由于我们使用的是 Bind() 方法,因此需要安装 Microsoft.Extensions.Configuration.Binder NuGet 包。
After that, we can modify the GetPrincipalFromExpiredToken method by removing the GetSection part and modifying the TokenValidationParameters object creation:
之后,我们可以通过删除 GetSection 部分并修改 TokenValidationParameters 对象创建来修改 GetPrincipalFromExpiredToken 方法:
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true,ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET"))), ValidateLifetime = true, ValidIssuer = _jwtConfiguration.ValidIssuer, ValidAudience = _jwtConfiguration.ValidAudience }; ... return principal; }
And let’s do a similar thing for the GenerateTokenOptions method:
让我们对 GenerateTokenOptions 方法执行类似的作:
private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List<Claim> claims) { var tokenOptions = new JwtSecurityToken ( issuer: _jwtConfiguration.ValidIssuer, audience: _jwtConfiguration.ValidAudience, claims: claims, expires: DateTime.Now.AddMinutes(Convert.ToDouble(_jwtConfiguration.Expires)), signingCredentials: signingCredentials ); return tokenOptions; }
Excellent.At this point, we can start our application and use both requests from Postman’s collection - 28-Refresh Token - to test our configuration.
非常好。此时,我们可以启动应用程序并使用来自 Postman 集合的两个请求 - 28-Refresh Token - 来测试我们的配置。
We should get the same responses as we did in a previous chapter, which proves that our configuration works as intended but now with a better code and less error-prone.
我们应该得到与上一章相同的响应,这证明我们的配置按预期工作,但现在代码更好,更不容易出错。
29.2 Options Pattern
29.2 选项模式
In the previous section, we’ve seen how we can bind configuration data to strongly typed objects. The options pattern gives us similar possibilities, but it offers a more structured approach and more features like validation, live reloading, and easier testing.
在上一节中,我们已经看到了如何将配置数据绑定到强类型对象。options 模式为我们提供了类似的可能性,但它提供了一种更结构化的方法和更多功能,如验证、实时重新加载和更轻松的测试。
Once we configure the class containing our configuration we can inject it via dependency injection with IOptions
一旦我们配置了包含我们的配置的类,我们就可以通过使用 IOptions 的依赖注入来注入它,从而只注入我们配置的一部分,或者更确切地说,只注入我们需要的部分。
If we need to reload the configuration without stopping the application, we can use the IOptionsSnapshot<T>
interface or the IOptionsMonitor<T>
interface depending on the situation. We’ll see when these interfaces should be used and why.
如果我们需要在不停止应用程序的情况下重新加载配置,我们可以根据情况使用 IOptionsSnapshot<T>
接口或 IOptionsMonitor<T>
接口。我们将了解何时应该使用这些接口以及为什么。
The options pattern also provides a good validation mechanism that uses the widely used DataAnotations attributes to check if the configuration abides by the logical rules of our application.
选项模式还提供了一种很好的验证机制,该机制使用广泛使用的 DataAnotations 属性来检查配置是否符合应用程序的逻辑规则。
The testing of options is also easy because of the helper methods and easy to mock options classes.
由于有辅助方法和易于模拟的选项类,选项的测试也很容易。
29.2.1 Using IOptions
29.2.1 使用 IOptions
We have already written a lot of code in the previous section that can be used with the IOptions interface, but we still have some more actions to do.
在上一节中,我们已经编写了大量可与 IOptions 接口一起使用的代码,但我们仍有一些作要执行。
The first thing we are going to do is to register and configure the JwtConfiguration class in the ServiceExtensions class:
我们要做的第一件事是在 ServiceExtensions 类中注册和配置 JwtConfiguration 类:
public static void AddJwtConfiguration(this IServiceCollection services, IConfiguration configuration) => services.Configure<JwtConfiguration>(configuration.GetSection("JwtSettings"));
And call this method in the Program class:
并在 Program 类中调用此方法:
builder.Services.ConfigureJWT(builder.Configuration); builder.Services.AddJwtConfiguration(builder.Configuration);
Since we can use IOptions with DI, we are going to modify the ServiceManager class to support that:
由于我们可以将 IOptions 与 DI 一起使用,因此我们将修改 ServiceManager 类以支持这一点:
public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper, IEmployeeLinks employeeLinks, UserManager<User> userManager, IOptions<JwtConfiguration> configuration)
We just replace the IConfiguration type with the IOptions type in the constructor.
我们只是在构造函数中将 IConfiguration 类型替换为 IOptions 类型。
For this, we need two additional namespaces:
为此,我们需要两个额外的命名空间:
using Entities.ConfigurationModels;
using Microsoft.Extensions.Options;
Then, we can modify the AuthenticationService’s constructor:
然后,我们可以修改 AuthenticationService 的构造函数:
private readonly ILoggerManager _logger; private readonly IMapper _mapper; private readonly UserManager<User> _userManager; private readonly IOptions<JwtConfiguration> _configuration; private readonly JwtConfiguration _jwtConfiguration; private User? _user; public AuthenticationService(ILoggerManager logger, IMapper mapper, UserManager<User> userManager, IOptions<JwtConfiguration> configuration) { _logger = logger; _mapper = mapper; _userManager = userManager; _configuration = configuration; _jwtConfiguration = _configuration.Value; }
And that’s it.
就是这样。
We inject IOptions inside the constructor and use the Value property to extract the JwtConfiguration object with all the populated properties. Nothing else has to change in this class.
我们在构造函数中注入 IOptions,并使用 Value 属性提取包含所有填充属性的 JwtConfiguration 对象。这个类中没有其他任何东西需要改变。
If we start the application again and send the same requests, we will still get valid results meaning that we’ve successfully implemented IOptions in our project.
如果我们再次启动应用程序并发送相同的请求,我们仍然会得到有效的结果,这意味着我们已经在项目中成功实现了 IOptions。
One more thing. We didn’t modify anything inside the ServiceExtensions/ConfigureJWT method. That’s because this configuration happens during the service registration and not after services are built. This means that we can’t resolve our required service here.
还有一件事。我们没有修改 ServiceExtensions/ConfigureJWT 方法中的任何内容。这是因为此配置发生在服务注册期间,而不是在构建服务之后。这意味着我们无法在此处解决所需的服务。
Well, to be precise, we can use the BuildServiceProvider method to build a service provider containing all the services from the provided IServiceCollection, and thus being able to access the required service. But if you do that, you will create one more list of singleton services, which can be quite expensive depending on the size of your application. So, you should be careful with this method.
嗯,准确地说,我们可以使用 BuildServiceProvider 方法构建一个服务提供程序,其中包含所提供的 IServiceCollection 中的所有服务,从而能够访问所需的服务。但是,如果您这样做,您将再创建一个单例服务列表,根据应用程序的大小,这可能会非常昂贵。因此,您应该小心使用此方法。
That said, using Binding to access configuration values is perfectly safe and cheap in this stage of the application’s lifetime.
也就是说,在应用程序生命周期的这个阶段,使用 Binding 来访问配置值是完全安全且成本低廉的。
29.2.2 IOptionsSnapshot and IOptionsMonitor
The previous code looks great but if we want to change the value of Expires to 10 instead of 5 for example, we need to restart the application to do it. You can imagine how useful would be to have a published application and all you need to do is to modify the value in the configuration file without restarting the whole app.
前面的代码看起来很棒,但是如果我们想将 Expires 的值更改为 10 而不是 5,则需要重新启动应用程序才能执行此作。您可以想象拥有一个已发布的应用程序会有多有用,您需要做的就是修改配置文件中的值,而无需重新启动整个应用程序。
Well, there is a way to do it by using IOptionsSnapshot or IOptionsMonitor.
嗯,有一种方法可以通过使用 IOptionsSnapshot 或 IOptionsMonitor 来实现。
All we would have to do is to replace the IOptions<JwtConfiguration>
type with the IOptionsSnapshot<JwtConfiguration>
or IOptionsMonitor<JwtConfiguration>
types inside the ServiceManager and AuthenticationService classes. Also if we use IOptionsMonitor, we can’t use the Value property but the CurrentValue.
我们所要做的就是将 IOptions<JwtConfiguration>
类型替换为 ServiceManager 和 AuthenticationService 类中的 IOptionsSnapshot<JwtConfiguration>
和 IOptionsMonitor<JwtConfiguration>
类型。此外,如果我们使用 IOptionsMonitor,则不能使用 Value 属性,而只能使用 CurrentValue。
So the main difference between these two interfaces is that the IOptionsSnapshot service is registered as a scoped service and thus can’t be injected inside the singleton service. On the other hand, IOptionsMonitor is registered as a singleton service and can be injected into any service lifetime.
因此,这两个接口之间的主要区别在于,IOptionsSnapshot 服务注册为范围服务,因此不能注入到单一实例服务中。另一方面,IOptionsMonitor 注册为单一实例服务,可以注入到任何服务生存期中。
To make the comparison even clearer, we have prepared the following list for you:
为了使比较更加清晰,我们为您准备了以下列表:
IOptions
• Is the original Options interface and it’s better than binding the whole Configuration
是原始的 Options 接口,比绑定整个 Configuration 要好
• Does not support configuration reloading
不支持重新加载配置
• Is registered as a singleton service and can be injected anywhere
注册为单例服务,可以在任何地方注入
• Binds the configuration values only once at the registration, and returns the same values every time
注册时仅绑定一次配置值,并且每次都返回相同的值
• Does not support named options
不支持命名选项
IOptionsSnapshot
• Registered as a scoped service
注册为范围服务
• Supports configuration reloading
支持配置重新加载
• Cannot be injected into singleton services
不能注入到单例服务
• Values reload per request
每个请求重新加载值
• Supports named options
支持命名选项
IOptionsMonitor
• Registered as a singleton service
注册为单例服务
• Supports configuration reloading
支持配置重新加载
• Can be injected into any service lifetime
可以注入任何使用寿命
• Values are cached and reloaded immediately
值会立即缓存并重新加载
• Supports named options
支持命名选项
Having said that, we can see that if we don’t want to enable live reloading or we don’t need named options, we can simply use IOptions<T>
. If we do, we can use either IOptionsSnapshot<T>
or IOptionsMonitor<T>
,but IOptionsMonitor<T>
can be injected into other singleton services while IOptionsSnapshot<T>
cannot.
话虽如此,我们可以看到,如果我们不想启用实时重新加载或不需要命名选项,我们可以简单地使用 IOptions<T>
。如果这样做,我们可以使用 IOptionsSnapshot<T>
或 IOptionsMonitor<T>
,但 IOptionsMonitor<T>
可以注入到其他单一实例服务中,而 IOptionsSnapshot<T>
则不能。
We have mentioned Named Options a couple of times so let’s explain what that is.
我们已经提到了几次 Named Options,所以让我们解释一下它是什么。
Let’s assume, just for example sake, that we have a configuration like this one:
让我们假设,只是为了说明原因,我们有一个这样的配置:
"JwtSettings": { "validIssuer": "CodeMazeAPI", "validAudience": "https://localhost:5001", "expires": 5 }, "JwtAPI2Settings": { "validIssuer": "CodeMazeAPI2", "validAudience": "https://localhost:5002", "expires": 10 },
Instead of creating a new JwtConfiguration2 class that has the same properties as our existing JwtConfiguration class, we can add another configuration:
我们可以添加另一个配置,而不是创建一个与现有 JwtConfiguration 类具有相同属性的新 JwtConfiguration2 类:
services.Configure<JwtConfiguration>("JwtSettings", configuration.GetSection("JwtSettings")); services.Configure<JwtConfiguration>("JwtAPI2Settings", configuration.GetSection("JwtAPI2Settings"));
Now both sections are mapped to the same configuration class, which makes sense. We don’t want to create multiple classes with the same properties and just name them differently. This is a much better way of doing it.
现在,这两个部分都映射到同一个 configuration class,这是有道理的。我们不想创建多个具有相同属性的类,然后只是以不同的方式命名它们。这是一种更好的方法。
Calling the specific option is now done using the Get method with a section name as a parameter instead of the Value or CurrentValue properties:
现在,使用 Get 方法将节名称作为参数,而不是 Value 或 CurrentValue 属性来调用特定选项:
_jwtConfiguration = _configuration.Get("JwtSettings");
That’s it. All the rest is the same.
就是这样。其余的都是一样的。