.NET 应用设置详解(C# & VB)





5.00/5 (12投票s)
为非 ASP.NET Core 应用启用开发和生产 AppSettings 支持
- 下载 AppSettings_v1.20 - 181.2 KB [新]
- 下载 AppSettings_v1.11 - 178 KB [已弃用]
- 下载 AppSettings_v1.01 - 166.6 KB [已弃用]
目录
引言
ASP.NET 应用程序通过环境驱动的多文件(appsettings.json、appsettings.Development.json 和 appsettings.Production.json)开箱即用地支持 开发和生产 设置。
appsettings.json 只是应用程序设置可以设置的众多地方之一。您可以在此处阅读更多相关信息:.NET 中的配置提供程序 | Microsoft Learn
本文将重点介绍为其他应用程序类型添加对 appsettings.json 的支持,特别是 Dot Net Core 控制台、Winforms 和 WPF 类型。
虽然不是关键,但为了更好地理解,我们将深入研究 Dot Net Core 框架的源代码,看看 Microsoft 的配置是如何工作的。我们将涵盖的内容已在上面的链接中记录。然后,我们将在一个 Console
应用程序中进行快速测试,以更好地理解如何以及为何。
注意:本文纯粹专注于 Dot Net Core,而非 .Net Framework。
这如何工作?
我们需要查看框架如何将 appsettings.json 绑定起来。为此,我们需要探索框架代码中的实现,特别是 HostingHostBuilderExtensions
类中的 Hostbuilder.ConfigureDefaults
。
internal static void ApplyDefaultHostConfiguration
(IConfigurationBuilder hostConfigBuilder, string[]? args)
{
/*
If we're running anywhere other than C:\Windows\system32,
we default to using the CWD for the ContentRoot.
However, since many things like Windows services and MSIX installers have
C:\Windows\system32 as there CWD which is not likely to really be
the home for things like appsettings.json, we skip
changing the ContentRoot in that case. The non-"default" initial
value for ContentRoot is AppContext.BaseDirectory
(e.g. the executable path) which probably
makes more sense than the system32.
In my testing, both Environment.CurrentDirectory and Environment.GetFolderPath(
Environment.SpecialFolder.System) return the path without
any trailing directory separator characters. I'm not even sure
the casing can ever be different from these APIs, but I think
it makes sense to ignore case for Windows path comparisons
given the file system is usually
(always?) going to be case insensitive for the system path.
*/
string cwd = Environment.CurrentDirectory;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
!string.Equals(cwd, Environment.GetFolderPath
(Environment.SpecialFolder.System),
StringComparison.OrdinalIgnoreCase))
{
hostConfigBuilder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string?>(HostDefaults.ContentRootKey, cwd),
});
}
hostConfigBuilder.AddEnvironmentVariables(prefix: "DOTNET_");
if (args is { Length: > 0 })
{
hostConfigBuilder.AddCommandLine(args);
}
}
在这里,我们可以看到使用带有 DOTNET_
前缀的 Environment
变量。
您可以在此处阅读更多相关信息:.NET Generic Host | Microsoft Learn。
要获取后缀,我们查看 IHostEnvironment
中 EnvironmentName
属性的注释。
/// <summary>
/// Provides information about the hosting environment an application is running in.
/// </summary>
public interface IHostEnvironment
{
/// <summary>
/// Gets or sets the name of the environment.
/// The host automatically sets this property
/// to the value of the "environment" key as specified in configuration.
/// </summary>
string EnvironmentName { get; set; }
/// <summary>
/// Gets or sets the name of the application.
/// This property is automatically set by the
/// host to the assembly containing the application entry point.
/// </summary>
string ApplicationName { get; set; }
/// <summary>
/// Gets or sets the absolute path to the directory that contains
/// the application content files.
/// </summary>
string ContentRootPath { get; set; }
/// <summary>
/// Gets or sets an <see cref="IFileProvider"/> pointing at
/// <see cref="ContentRootPath"/>.
/// </summary>
IFileProvider ContentRootFileProvider { get; set; }
}
所以后缀是 Environment
。因此,完整的环境变量名称是 DOTNET_ENVIRONMENT
。与 ASP.NET 及其 ASPNETCORE_ENVIRONMENT
变量不同,DOTNET_ENVIRONMENT
默认情况下未设置。
当未设置环境变量时,EnviromentName
默认为 Production
,如果存在,则为 Debug
和 Release
模式使用 appsettings.Production.json,即使 appsettings.Development.json 存在。appsettings.Development.json 将被忽略。
运行时如何合并设置
我们需要查看 HostingHostBuilderExtensions
类中的另一个扩展方法。
internal static void ApplyDefaultAppConfiguration(
HostBuilderContext hostingContext,
IConfigurationBuilder appConfigBuilder, string[]? args)
{
IHostEnvironment env = hostingContext.HostingEnvironment;
bool reloadOnChange = GetReloadConfigOnChangeValue(hostingContext);
appConfigBuilder
.AddJsonFile("appsettings.json", optional: true,
reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true,
reloadOnChange: reloadOnChange);
if (env.IsDevelopment() && env.ApplicationName is { Length: > 0 })
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly is not null)
{
appConfigBuilder.AddUserSecrets(appAssembly, optional: true,
reloadOnChange: reloadOnChange);
}
}
appConfigBuilder.AddEnvironmentVariables();
if (args is { Length: > 0 })
{
appConfigBuilder.AddCommandLine(args);
}
我们感兴趣的是这个。
appConfigBuilder.AddJsonFile("appsettings.json", optional: true,
reloadOnChange: reloadOnChange) .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: reloadOnChange);
在这里,我们可以看到 appsettings.json 首先加载,然后是 $"appsettings.{env.EnvironmentName}.json",其中 EnvironmentName
取决于 DOTNET_ENVIRONMENT
环境变量。正如我们上面所知,除非我们手动选择一个,否则 EnvironmentName
将默认为 Production
。
如果设置了,appsettings.json 中的任何设置都将被 appsettings.Production.json 的值覆盖。
为开发和生产环境配置
要设置 DOTNET_ENVIRONMENT
变量,请右键单击 **解决方案资源管理器** 中的应用程序名称,打开应用程序的“**属性**”,导航到“**调试**”>“**常规**”部分,然后单击“**打开调试启动配置文件 UI**”。这将打开 **启动配置文件** 窗口。
或者,我们可以从工具栏访问启动配置文件窗口。
我们感兴趣的是“**环境变量**”。您可以在此处设置任何内容。我们需要添加的是名称:DOTNET_ENVIRONMENT
,值为:Development
。没有关闭按钮,只需关闭窗口,然后从 VS 菜单中选择“**文件**”>“**保存...**”。
这会在解决方案文件夹的根目录下添加一个名为 Properties 的新文件夹,并创建 launchSettings.json 文件,内容如下:
{
"profiles": {
"[profile_name_goes_here]": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "DEVELOPMENT"
}
}
}
}
注意:launchSettings.json 文件仅适用于 Visual Studio。它不会被复制到您的编译文件夹。如果您将其包含在应用程序中,运行时将忽略它。您需要在应用程序中手动支持此文件,以便在 Visual Studio 外部使用。
设置配置设置
我们希望为开发和生产提供不同的设置,因此我们配置文件。
- appsettings.json - 此文件包含两个环境的根配置。
- appsettings.Development.json - 开发期间使用的设置。
- appsettings.Production.json - 部署的实时应用程序使用的设置。
注意
- 所有 appsettings 文件都需要标记为 **内容** 并 **如果较新则复制**,以便在 Debug 和 Release 模式下使用。
- 由于所有 appsettings 文件都将位于 Release 文件夹中,因此您需要配置安装程序,使其仅打包所需的文件 - 不要包含 appsettings.Development.json 文件。
您可以设置多个启动配置文件。上面我设置了三个(3)个,每个都有自己的环境变量设置。
- 开发 -
Development
- 暂存 -
Staging
- 生产 - 无(默认为
Production
)
要选择一个配置文件进行测试,我们可以从工具栏中选择。
文件:“appsettings.json”
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
文件:“appsettings.Development.json”
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"System.Net.Http.HttpClient": "Trace"
}
}
}
文件:“appsettings.Production.json”
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"System.Net.Http.HttpClient": "Warning"
}
}
}
如果只有 appsettings.json,则日志记录至少为 Information
级别。
如果在 Debug 模式下,则 appsettings.Development.json 启用所有日志记录。
如果在 Release 模式下,则 appsettings.Production.json,日志记录为 Warning
、Error
和 Critical
级别。
同样适用于 appsettings.json 文件中设置的其他应用程序选项。
实现
我们将创建一个 Console
应用程序来查看上面学到的内容。我们将使用 appsettings
中的示例选项,并将它们映射到一个选项类。代码极简,以保持实现易于理解。
设置
我们需要准备选项。我们将需要一个类来映射 appsettings 文件中的选项部分设置。
- 我们的 **设置选项** 类。
public class MySectionOptions { public string? Setting1 { get; set; } public int? Setting2 { get; set; } }
Public Class MySectionOptions Property Setting1 As String Property Setting2 As Integer End Class
- 添加到
appsettings
文件。现在为不同的配置设置选项。
- appsettings.json 文件
{ "MySection": { "setting1": "default_value_1", "setting2": 222 } }
- appsettings.Development.json 文件
{ "MySection": { "setting1": "development_value_1", "setting2": 111 } }
- appsettings.Production.json 文件
{ "MySection": { "setting1": "production_value_1", "setting2": 333 } }
- appsettings.json 文件
示例控制台应用程序(依赖注入)
现在我们可以从 appsettings
中读取选项。
1. 设置依赖注入
IHostBuilder builder = Host.CreateDefaultBuilder();
// Map the options class to the section in the `appsettings`
builder.ConfigureServices((context, services) =>
{
IConfiguration configRoot = context.Configuration;
services.Configure<MySectionOptions>
(configRoot.GetSection("MySection"));
});
IHost host = builder.Build();
Dim builder As IHostBuilder = Host.CreateDefaultBuilder()
' Map the options class to the section in the `appsettings`
builder.ConfigureServices(
Sub(context, services)
Dim configRoot As IConfiguration = context.Configuration
services.Configure(Of MySectionOptions)(configRoot.GetSection("MySection"))
End Sub)
Dim _host As IHost = builder.Build()
注意:我们已经定义了我们将检索强类型选项部分以使用选项模式。
2. 检索强类型选项
// If environment variable not set, will default to "Production"
string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
Console.WriteLine($"Environment: {env}");
// Get the options from `appsettings`
MySectionOptions options = host.Services
.GetRequiredService<IOptions<MySectionOptions>>()
.Value;
Console.WriteLine($"Setting1: {options.Setting1}");
Console.WriteLine($"Setting2: {options.Setting2}");
Console.ReadKey();
' If environment variable not set, will default to "Production"
Dim env As String = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
If String.IsNullOrWhiteSpace(env) Then
env = "Production"
End If
Console.WriteLine($"Environment: {env}")
' Get the options from `appsettings`
Dim options As MySectionOptions = _host.Services _
.GetRequiredService(Of IOptions(Of MySectionOptions)) _
.Value
Console.WriteLine($"Setting1: {options.Setting1}")
Console.WriteLine($"Setting2: {options.Setting2}")
Console.ReadKey()
3. 检索单个值
上面,我们已经了解了如何检索强类型部分。如果我们只对单个键值对感兴趣该怎么办。我们也可以做到。
// manually retrieve values
IConfiguration config = host.Services.GetRequiredService<IConfiguration>();
// Log Level section
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]");
IConfigurationSection logLevel = config.GetSection("Logging:LogLevel:Default");
Console.WriteLine($" LogLevel: {logLevel.Value}");
Console.WriteLine();
// Option Settings section
Console.WriteLine("By Individual Option keys");
IConfigurationSection setting1 = config.GetSection("MySection:setting1");
Console.WriteLine($" Setting1: {setting1.Value}");
IConfigurationSection setting2 = config.GetSection("MySection:setting2");
Console.WriteLine($" Setting2: {setting2.Value}");
' manually retrieve values
dim config As IConfiguration = _host.Services.GetRequiredService(of IConfiguration)
' Log Level section
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]")
dim logLevel = config.GetSection("Logging:LogLevel:Default")
Console.WriteLine($" LogLevel: {logLevel.Value}")
Console.WriteLine()
' Option Settings section
Console.WriteLine("By Individual Option keys")
dim setting1 As IConfigurationSection = config.GetSection("MySection:setting1")
Console.WriteLine($" Setting1: {setting1.Value}")
dim setting2 as IConfigurationSection = config.GetSection("MySection:setting2")
Console.WriteLine($" Setting2: {setting2.Value}")
注意:要读取单个键值对,我们需要获取对 **DI 选项配置** 的引用,然后检索信息。
示例控制台应用程序(无依赖注入)
并非每个人都使用依赖注入,因此我创建了一个辅助类来抽象读取 appsettings.json 配置信息所需的代码。
需要以下 NuGet 包。
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration.EnvironmentVariables
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Options.ConfigurationExtensions
public class AppSettings<TOption>
{
#region Constructors
public AppSettings(IConfigurationSection configSection, string? key = null)
{
_configSection = configSection;
GetValue(key);
}
#endregion
#region Fields
protected static AppSettings<TOption>? _appSetting;
protected static IConfigurationSection? _configSection;
#endregion
#region Properties
public TOption? Value { get; set; }
#endregion
#region Methods
public static TOption? Current(string section, string? key = null)
{
_appSetting = GetCurrentSettings(section, key);
return _appSetting.Value;
}
public static AppSettings<TOption>
GetCurrentSettings(string section, string? key = null)
{
string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ??
"Production";
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
IConfigurationRoot configuration = builder.Build();
if (string.IsNullOrEmpty(section))
section = "AppSettings"; // default
AppSettings<TOption> settings =
new AppSettings<TOption>(configuration.GetSection(section), key);
return settings;
}
protected virtual void GetValue(string? key)
{
if (key is null)
{
// no key, so must be a class/strut object
Value = Activator.CreateInstance<TOption>();
_configSection.Bind(Value);
return;
}
Type optionType = typeof(TOption);
if ((optionType == typeof(string) ||
optionType == typeof(int) ||
optionType == typeof(long) ||
optionType == typeof(decimal) ||
optionType == typeof(float) ||
optionType == typeof(double))
&& _configSection != null)
{
// we must be retrieving a value
Value = _configSection.GetValue<TOption>(key);
return;
}
// Could not find a supported type
throw new InvalidCastException($"Type {typeof(TOption).Name} is invalid");
}
#endregion
}
Public Class AppSettings(Of TOption)
#Region "Constructors"
Public Sub New(configSection As IConfigurationSection, _
Optional key As String = Nothing)
_configSection = configSection
GetValue(key)
End Sub
#End Region
#Region "Fields"
Protected Shared _appSetting As AppSettings(Of TOption)
Protected Shared _configSection As IConfigurationSection
#End Region
#Region "Properties"
Public Property Value As TOption
#End Region
#Region "Methods"
Public Shared Function Current(section As String, _
Optional key As String = Nothing) As TOption
_appSetting = GetCurrentSettings(section, key)
Return _appSetting.Value
End Function
Public Shared Function GetCurrentSettings(section As String, _
Optional key As String = Nothing) As AppSettings(Of TOption)
Dim env As String = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
If String.IsNullOrWhiteSpace(env) Then
env = "Production"
End If
Dim builder As IConfigurationBuilder = New ConfigurationBuilder() _
.SetBasePath(Directory.GetCurrentDirectory()) _
.AddJsonFile("appsettings.json", optional:=True, reloadOnChange:=True) _
.AddJsonFile($"appsettings.{env}.json", _
optional:=True, reloadOnChange:=True) _
.AddEnvironmentVariables()
Dim configuration As IConfigurationRoot = builder.Build()
If String.IsNullOrEmpty(section) Then
section = "AppSettings" ' Default
End If
Dim settings As AppSettings(Of TOption) = _
New AppSettings(Of TOption)(configuration.GetSection(section), key)
Return settings
End Function
Protected Overridable Sub GetValue(Optional key As String = Nothing)
If key Is Nothing Then
' no key, so must be a class/strut object
Value = Activator.CreateInstance(Of TOption)
_configSection.Bind(Value)
Return
End If
Dim optionType As Type = GetType(TOption)
If (optionType Is GetType(String) OrElse
optionType Is GetType(Integer) OrElse
optionType Is GetType(Long) OrElse
optionType Is GetType(Decimal) OrElse
optionType Is GetType(Single) OrElse
optionType Is GetType(Double)) _
AndAlso _configSection IsNot Nothing Then
' we must be retrieving a value
Value = _configSection.GetValue(Of TOption)(key)
Return
End If
' Could not find a supported type
Throw New InvalidCastException($"Type {GetType(TOption).Name} is invalid")
End Sub
#End Region
End Class
注意
AppSettings
辅助类可与Option
类和单个键值一起使用。
使用辅助类非常简单。
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]");
string? logLevel = AppSettings<string>.Current("Logging:LogLevel", "Default");
Console.WriteLine($" LogLevel: {logLevel}");
Console.WriteLine();
Console.WriteLine("By Individual keys");
string? setting1 = AppSettings<string>.Current("MySection", "Setting1");
int? setting2 = AppSettings<int>.Current("MySection", "Setting2");
Console.WriteLine($" Setting1: {setting1}");
Console.WriteLine($" Setting2: {setting2}");
Console.WriteLine();
Console.WriteLine("By Option Class");
MySectionOptions? options = AppSettings<MySectionOptions>.Current("MySection");
Console.WriteLine($" Setting1: {options?.Setting1}");
Console.WriteLine($" Setting2: {options?.Setting2}");
Console.ReadKey();
Console.WriteLine("By Individual LogLevel key [Logging:LogLevel:Default]")
dim logLevel = AppSettings(Of String).Current("Logging:LogLevel", "Default")
Console.WriteLine($" LogLevel: {logLevel}")
Console.WriteLine()
Console.WriteLine("By Individual keys")
Dim setting1 = AppSettings(Of String).Current("MySection", "Setting1")
Dim setting2 = AppSettings(Of Integer).Current("MySection", "Setting2")
Console.WriteLine($" Setting1: {setting1}")
Console.WriteLine($" Setting2: {setting2}")
Console.WriteLine()
Console.WriteLine("By Option Class")
Dim options = AppSettings(Of MySectionOptions).Current("MySection")
Console.WriteLine($" Setting1: {options.Setting1}")
Console.WriteLine($" Setting2: {options.Setting2}")
Console.ReadKey()
如何测试
我们将逐步配置设置,以了解它们在 Debug
和 Release
模式下的工作方式。
- 仅使用 appsettings.json 文件进行测试。
Environment: Production By Individual LogLevel key [Logging:LogLevel:Default] LogLevel: Information By Individual Option keys Setting1: default_value_1 Setting2: 222 By Option Class Setting1: default_value_1 Setting2: 222
-
现在包含 appsettings.Production.json 文件。
Environment: Production By Individual LogLevel key [Logging:LogLevel:Default] LogLevel: Warning By Individual Option keys Setting1: production_value_1 Setting2: 333 By Option Class Setting1: production_value_1 Setting2: 333
-
现在包含 appsettings.Develpment.json 文件。
Environment: Production By Individual LogLevel key [Logging:LogLevel:Default] LogLevel: Warning By Individual Option keys Setting1: production_value_1 Setting2: 333 By Option Class Setting1: production_value_1 Setting2: 333
-
设置 launchSetting.json 文件。
Environment: Development By Individual LogLevel key [Logging:LogLevel:Default] LogLevel: Trace By Individual Option keys Setting1: development_value_1 Setting2: 111 By Option Class Setting1: development_value_1 Setting2: 111
但是等等,对于
Release
模式下的测试 4,并在未调试的情况下启动,我们仍然看到Environment: Development
。这是因为 launchSetting.json 中的环境变量。
我们需要转到命令行,并在 bin\Release\net7.0 文件夹中运行应用程序,然后我们将看到以下输出。
Environment: Production
By Individual LogLevel key [Logging:LogLevel:Default]
LogLevel: Warning
By Individual Option keys
Setting1: production_value_1
Setting2: 333
By Option Class
Setting1: production_value_1
Setting2: 333
或者,我们可以设置另一个启动配置文件来模拟 **生产** / **发布** 模式 - 请参阅 为开发和生产环境配置。
结论
虽然 **ASP.NET** Core 开箱即用地支持此功能,但我们可以通过在 launchsettings.json 中添加自己的环境变量,然后设置 appsettings 文件,将相同的行为添加到我们自己的 **控制台**、**Winforms** 或 **WPF** 应用程序中。
参考文献
- .NET 中的选项模式
- .NET 中的配置提供程序 | Microsoft Learn
- .NET Generic Host | Microsoft Learn
- HostingHostBuilderExtensions.cs | Github - dotnet/dotnet
- 文章图片来自:如何拍摄夜间火箭发射 | Photography Life
历史
- 2023年2月12日 - v1.00 - 初始发布
- 2023年2月14日 - v1.10 - 添加了“示例控制台应用程序(无依赖注入)”部分,使用了
AppSettings
辅助类。 - 2023年2月16日 - v1.11 - 优化了
AppSettings
辅助类。 - 2023年2月25日 - v1.20 - 添加了文档,并更新了示例代码,以演示如何处理强类型选项和嵌套部分,以及如何与单个键值对配合使用。
- 2023年2月28日 - v1.21 - 添加了关于使用 选项模式 的必要澄清。