当您有一个庞大的项目,一个庞大的传感器,量度和开关面板时,每个程序员都可以想象-或可能想像自己是一名飞机飞行员,您可以轻松地按需配置所有内容。好吧,至少不能自己手动抬起机箱。度量标准和图形都很好,但是今天我想向您介绍可以改变飞机行为参数,配置它的那些翻转开关和按钮。
配置的重要性很难低估。每个人都使用一种或另一种方法来配置他们的应用程序,原则上没有什么复杂的事情,但是真的那么简单吗?我建议查看配置中的“之前”和“之后”,并了解详细信息:如何工作,我们拥有哪些新功能以及如何充分利用它们。那些不熟悉.NET Core配置的人将掌握基础知识,而那些不熟悉.NET Core的人将获得食物,可以在日常工作中考虑和使用新方法。
.NET Core之前的配置
2002年,引入了.NET Framework,并且由于当时是XML的炒作,Microsoft的开发人员决定“让它随处可见”,结果,我们得到了仍然有效的XML配置。在表的顶部,我们有一个静态ConfigurationManager类,通过它可以获取参数值的字符串表示形式。配置本身看起来像这样:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="Title" value=".NET Configuration evo" />
<add key="MaxPage" value="10" />
</appSettings>
</configuration>
解决了问题后,开发人员获得了自定义选项,该选项比INI文件要好,但有其自身的特点。因此,例如,使用配置文件的XSLT转换来实现对不同类型的应用程序环境的不同设置值的支持。如果我们希望对数据进行分组更复杂,则可以为元素和属性定义自己的XML模式。键值对具有严格的字符串类型,如果我们需要一个数字或一个日期,则“让我们自己以某种方式来做”:
string title = ConfigurationManager.AppSettings["Title"];
int maxPage = int.Parse(ConfigurationManager.AppSettings["MaxPage"]);
在2005年,我们添加了配置部分,它们允许对参数进行分组,构建自己的方案,避免命名冲突。我们还介绍了* .settings文件和一个特别的设计器。
现在,您可以获取一个生成的,类型强的类,该类表示配置数据。设计器使您可以方便地编辑值,可以按编辑器列进行排序。使用生成的类的Default属性检索数据,该属性提供Singleton配置对象。
DateTime date = Properties.Settings.Default.CustomDate;
int displayItems = Properties.Settings.Default.MaxDisplayItems;
string name = Properties.Settings.Default.ApplicationName;
我们还添加了配置参数值的范围。用户区域负责用户数据,用户数据可以由他更改并在程序执行期间保存。保存将在单独的文件中沿路径%AppData%\ *应用程序名称*进行。Application范围使您无需用户重新定义即可检索参数值。
尽管有了良好的意图,整个事情变得更加复杂。
- 实际上,这些是相同的XML文件,其大小开始增长得更快,结果导致读取不便。
- 从XML文件中读取一次配置,我们需要重新加载应用程序以将更改应用于配置数据。
- 从* .settings文件生成的类已用seal修饰符标记,因此无法继承该类。此外,可以更改此文件,但是如果重新生成,我们将丢失自己编写的所有内容。
- 仅根据键值方案处理数据。要获得使用结构的结构化方法,我们需要自己另外实现。
- 数据源只能是文件,不支持外部提供程序。
- 另外,我们有一个人为因素-私有参数进入版本控制系统并公开。
到目前为止,所有这些问题仍然存在于.NET Framework中。
.NET Core配置
在.NET Core中,他们重新构想了配置并从头开始创建了所有内容,删除了静态ConfigurationManager类,并解决了许多“以前”的问题。我们得到了什么新东西?和以前一样-生成配置数据的阶段和使用此数据的阶段,但是具有更灵活和更长的生命周期。
设置并填充配置数据
因此,在数据生成阶段,我们可以使用许多资源,而不仅限于文件。配置是通过IConfgurationBuilder完成的-我们可以在此基础上添加数据源。NuGet软件包可用于各种类型的源:
格式 | 将源添加到IConfigurationBuilder的扩展方法 | NuGet包 |
杰森 | 添加JsonFile | Microsoft.Extensions.Configuration.Json |
XML格式 | AddXmlFile | Microsoft.Extensions.Configuration.Xml |
尼尼 | AddIniFile | Microsoft.Extensions.Configuration.Ini |
命令行参数 | AddCommandLine | Microsoft.Extensions.Configuration.CommandLine |
环境变量 | AddEnvironmentVariables | Microsoft.Extensions.Configuration.EnvironmentVariables |
用户机密 | AddUserSecrets | Microsoft.Extensions.Configuration.UserSecrets |
密钥文件 | AddKeyPerFile | Microsoft.Extensions.Configuration.KeyPerFile |
Azure KeyVault | AddAzureKeyVault | Microsoft.Extensions.Configuration.AzureKeyVault |
每个源均作为新层添加,并使用匹配键覆盖参数。这是ASP.NET Core应用程序模板(版本3.1)中默认提供的Program.cs示例。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
{ webBuilder.UseStartup<Startup>(); });
我想把主要重点放在CreateDefaultBuilder上。在方法内部,我们将看到源的初始配置是如何发生的。
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new WebHostBuilder();
...
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
IHostingEnvironment env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsDevelopment())
{
Assembly appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
...
return builder;
}
因此,我们认为整个配置的基础将是appsettings.json文件;此外,如果有针对特定环境的文件,则它将具有更高的优先级,从而覆盖基础的匹配值。以及随后的每个来源。加法顺序影响最终值。从外观上看,一切看起来都像这样:
如果您想使用订单,则只需清除订单并定义需要的方式即可。
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
.ConfigureAppConfiguration((context,
builder) =>
{
builder.Sources.Clear();
//
});
每个配置源都有两个部分:
- IConfigurationSource的实现。提供配置值的来源。
- IConfigurationProvider的实现。将原始数据转换为结果键值。
通过实现这些组件,我们可以获得自己的数据源进行配置。这是一个示例,说明如何通过Entity Framework从数据库获取参数。
如何使用和检索数据
现在,设置和填充配置数据已经很清楚了,我建议看看如何使用这些数据以及如何更方便地获取它。配置项目的新方法对流行的JSON格式有很大的偏见,这并不奇怪,因为有了它的帮助,我们可以构建任何数据结构,对数据进行分组并同时具有可读文件。以以下配置文件为例:
{
"Features" : {
"Dashboard" : {
"Title" : "Default dashboard",
"EnableCurrencyRates" : true
},
"Monitoring" : {
"EnableRPSLog" : false,
"EnableStorageStatistic" : true,
"StartTime": "09:00"
}
}
}
所有数据形成一个平面键值字典,配置键由每个值的整个文件键层次结构形成。一个类似的结构将具有以下数据集:
功能:仪表板:标题 | 默认仪表板 |
功能:仪表板:EnableCurrencyRates | 真正 |
功能:监视:EnableRPSLog | 假 |
功能:监视:EnableStorageStatistic | 真正 |
功能:监视:StartTime | 09:00 |
我们可以使用IConfiguration对象获取值。例如,这是我们如何获取参数的方法:
string title = Configuration["Features:Dashboard:Title"];
string title1 = Configuration.GetValue<string>("Features:Dashboard:Title");
bool currencyRates = Configuration.GetValue<bool>("Features:Dashboard:EnableCurrencyRates");
bool enableRPSLog = Configuration.GetValue<bool>("Features:Monitoring:EnableRPSLog");
bool enableStorageStatistic = Configuration.GetValue<bool>("Features:Monitoring:EnableStorageStatistic");
TimeSpan startTime = Configuration.GetValue<TimeSpan>("Features:Monitoring:StartTime");
而且这已经不错了,我们有一种很好的方法来获取转换为所需数据类型的数据,但是某种程度上却不如我们所愿。如果我们收到上面给出的数据,那么我们将得到一个重复的代码,并在键名上犯错误。您可以组装一个完整的配置对象,而不是单个值。通过Bind方法将数据绑定到对象将帮助我们。类和数据检索的示例:
public class MonitoringConfig
{
public bool EnableRPSLog { get; set; }
public bool EnableStorageStatistic { get; set; }
public TimeSpan StartTime { get; set; }
}
var monitorConfiguration = new MonitoringConfig();
Configuration.Bind("Features:Monitoring", monitorConfiguration);
var monitorConfiguration1 = new MonitoringConfig();
IConfigurationSection configurationSection = Configuration.GetSection("Features:Monitoring");
configurationSection.Bind(monitorConfiguration1);
在第一种情况下,我们按节名称进行绑定,在第二种情况下,我们获得节并从中进行绑定。本部分允许您使用配置的部分视图-通过这种方式,您可以控制正在使用的数据集。这些节也用于标准扩展方法中-例如,使用“ ConnectionStrings”节获取连接字符串。
string connectionString = Configuration.GetConnectionString("Default");
public static string GetConnectionString(this IConfiguration configuration, string name)
{
return configuration?.GetSection("ConnectionStrings")?[name];
}
选项-键入配置视图
手动创建配置对象并绑定到数据是不切实际的,但是存在使用Options形式的解决方案。选项用于获取配置的强类型视图。视图类必须是公共的,没有构造函数,并且没有用于分配值的参数和公共属性,该对象通过反射来填充。可以在源代码中找到更多详细信息。
要开始使用Options,我们需要使用IServiceCollection的Configure扩展方法注册配置类型,以指示要投影到类上的部分。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<MonitoringConfig>(Configuration.GetSection("Features:Monitoring"));
}
之后,我们可以通过注入对IOptions,IOptionsMonitor,IOptionsSnapshot接口的依赖来接收配置。我们可以通过Value属性从IOptions接口获取MonitoringConfig对象。
public class ExampleService
{
private IOptions<MonitoringConfig> _configuration;
public ExampleService(IOptions<MonitoringConfig> configuration)
{
_configuration = configuration;
}
public void Run()
{
TimeSpan timeSpan = _configuration.Value.StartTime; // 09:00
}
}
IOptions接口的功能是,在依赖项注入容器中,配置已在Singleton生命周期中注册为对象。值属性首次请求值时,将使用存在的数据初始化对象,只要该对象存在即可。 IOptions不支持数据刷新。有IOptionsSnapshot和IOptionsMonitor接口可支持更新。
DI容器中的IOptionsSnapshot已在Scoped生命周期中注册,这使得可以根据请求使用新的容器范围获取新的配置对象。例如,在一个Web请求期间,我们将收到相同的对象,但对于新请求,我们将收到一个具有更新数据的新对象。
IOptionsMonitor被注册为Singleton,唯一的区别是在请求时,每个配置都随实际数据一起接收。另外,如果您需要响应数据更改事件本身,则IOptionsMonitor允许您注册配置更改事件处理程序。
public class ExampleService
{
private IOptionsMonitor<MonitoringConfig> _configuration;
public ExampleService(IOptionsMonitor<MonitoringConfig> configuration)
{
_configuration = configuration;
configuration.OnChange(config =>
{
Console.WriteLine(" ");
});
}
public void Run()
{
TimeSpan timeSpan = _configuration.CurrentValue.StartTime; // 09:00
}
}
也可以按名称获取IOptionsSnapshot和IOptionsMontitor-如果您有多个与一个类相对应的配置节,并且想要获得一个特定的类,则这是必需的。例如,我们有以下数据:
{
"Cache": {
"Main": {
"Type": "global",
"Interval": "10:00"
},
"Partial": {
"Type": "personal",
"Interval": "01:30"
}
}
}
用于投影的类型:
public class CachePolicy
{
public string Type { get; set; }
public TimeSpan Interval { get; set; }
}
我们使用特定名称注册配置:
services.Configure<CachePolicy>("Main", Configuration.GetSection("Cache:Main"));
services.Configure<CachePolicy>("Partial", Configuration.GetSection("Cache:Partial"));
我们可以收到如下值:
public class ExampleService
{
public ExampleService(IOptionsSnapshot<CachePolicy> configuration)
{
CachePolicy main = configuration.Get("Main");
TimeSpan mainInterval = main.Interval; // 10:00
CachePolicy partial = configuration.Get("Partial");
TimeSpan partialInterval = partial.Interval; // 01:30
}
}
如果查看我们用来注册配置类型的扩展方法的源代码,您会看到默认名称为Options.Default,这是一个空字符串。因此,我们总是隐式地为配置传递名称。
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
=> services.Configure<TOptions>(Options.Options.DefaultName, config);
由于配置可以由类表示,因此我们还可以通过使用System.ComponentModel.DataAnnotations命名空间中的验证属性标记属性来添加参数值验证。例如,我们指定Type属性的值必须是必需的。但是我们还需要在注册配置时指出原则上应该进行验证。为此有一个扩展方法ValidateDataAnnotations。
public class CachePolicy
{
[Required]
public string Type { get; set; }
public TimeSpan Interval { get; set; }
}
services.AddOptions<CachePolicy>()
.Bind(Configuration.GetSection("Cache:Main"))
.ValidateDataAnnotations();
这种验证的特殊性在于,只有在收到配置对象时才会进行验证。这使得很难理解应用程序启动时配置无效。GitHub上有一个针对此问题的问题。解决此问题的一种方法可以是文章“在ASP.NET Core中向强类型配置对象添加验证”中介绍的方法。
选项的缺点以及如何解决它们
通过选项进行配置也有其缺点。为了使用,我们需要添加一个依赖项,并且每次我们需要访问Value / CurrentValue属性来获取值对象时。您可以通过获取没有Option包装程序的干净配置对象来获得更干净的代码。该问题的最简单解决方案可能是在纯配置类型依赖项的容器中进行其他注册。
services.Configure<MonitoringConfig>(Configuration.GetSection("Features:Monitoring"));
services.AddScoped<MonitoringConfig>(provider => provider.GetRequiredService<IOptionsSnapshot<MonitoringConfig>>().Value);
解决方案很简单,我们不强迫最终代码了解IOptions,但是如果需要它们,我们将失去灵活性,无法执行其他配置操作。为了解决这个问题,我们可以使用“ Bridge”模式,这将使我们获得一个额外的层,在接收对象之前,我们可以在其中执行额外的动作。
为了实现这个目标,我们需要重构当前的示例代码。由于配置类具有不带参数的构造函数形式的限制,因此我们无法将IOptions / IOptionsSnapshot / IOptionsMontitor对象传递给构造函数;为此,我们将从最终视图中分离配置读数。
例如,假设我们要指定MonitoringConfig类的StartTime属性,该字符串的分钟表示形式为分钟,其值为“ 09”,不适合标准格式。
public class MonitoringConfigReader
{
public bool EnableRPSLog { get; set; }
public bool EnableStorageStatistic { get; set; }
public string StartTime { get; set; }
}
public interface IMonitoringConfig
{
bool EnableRPSLog { get; }
bool EnableStorageStatistic { get; }
TimeSpan StartTime { get; }
}
public class MonitoringConfig : IMonitoringConfig
{
public MonitoringConfig(IOptionsMonitor<MonitoringConfigReader> option)
{
MonitoringConfigReader reader = option.Value;
EnableRPSLog = reader.EnableRPSLog;
EnableStorageStatistic = reader.EnableStorageStatistic;
StartTime = GetTimeSpanValue(reader.StartTime);
}
public bool EnableRPSLog { get; }
public bool EnableStorageStatistic { get; }
public TimeSpan StartTime { get; }
private static TimeSpan GetTimeSpanValue(string value) => TimeSpan.ParseExact(value, "mm", CultureInfo.InvariantCulture);
}
为了获得干净的配置,我们需要在依赖项注入容器中注册它。
services.Configure<MonitoringConfigReader>(Configuration.GetSection("Features:Monitoring"));
services.AddTransient<IMonitoringConfig, MonitoringConfig>();
这种方法使您可以创建一个完全独立的生命周期,以形成配置对象。如果您以加密形式收到数据,则可以添加自己的数据验证,或另外实施数据解密阶段。
确保数据安全
一个重要的配置任务是数据安全性。文件配置不安全,因为数据以明文形式存储,易于阅读;通常,文件与应用程序位于同一目录中。错误地,您可以将值提交给版本控制系统,该系统可以对数据进行解密,但是可以想象这是否是公共代码!这种情况非常普遍,以至于甚至没有现成的工具可以找到这种泄漏-Gitleaks。另有一篇文章提供了统计信息和各种公开数据。
通常,项目必须具有用于不同环境的单独参数(发布/调试等)。例如,作为解决方案之一,您可以使用持续集成和交付工具替换最终值,但是此选项在设计时不保护数据。用户秘密工具旨在保护开发人员。它包含在.NET Core SDK(3.0.100及更高版本)中。该工具的主要原理是什么?首先,我们需要初始化项目以使用init命令。
dotnet user-secrets init
该命令将UserSecretsId元素添加到.csproj项目文件中。使用此参数,我们将获得一个私有存储,它将存储常规的JSON文件。区别在于它不在您的项目目录中,因此仅在当前计算机上可用。 Windows的路径是%APPDATA%\ Microsoft \ UserSecrets \ <user_secrets_id> \ secrets.json,而Linux和MacOS的路径是〜/ .microsoft / usersecrets / <user_secrets_id> /secrets.json。我们可以使用set命令将上述示例中的值添加:
dotnet user-secrets set "Features:Monitoring:StartTime" "09:00"
可用命令的完整列表可以在文档中找到。
使用专用存储(例如:AWS Secrets Manager,Azure Key Vault,HashiCorp Vault,Consul和ZooKeeper)可以最好地确保生产中的数据安全。为了连接某些组件,已经有现成的NuGet软件包,对于某些组件,可以访问REST API,因此很容易自己实现它们。
结论
现代问题需要现代解决方案。随着从整体式转向动态基础架构,配置方法也发生了变化。无论配置数据源的位置和类型如何,都需要对数据更改进行快速响应。与.NET Core一起,我们获得了用于实现各种应用程序配置方案的好工具。