65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2023年2月12日

CPOL

7分钟阅读

viewsIcon

36933

downloadIcon

768

为非 ASP.NET Core 应用启用开发和生产 AppSettings 支持

目录

引言

ASP.NET 应用程序通过环境驱动的多文件(appsettings.jsonappsettings.Development.jsonappsettings.Production.json)开箱即用地支持 开发和生产 设置。

appsettings.json 只是应用程序设置可以设置的众多地方之一。您可以在此处阅读更多相关信息:.NET 中的配置提供程序 | Microsoft Learn

本文将重点介绍为其他应用程序类型添加对 appsettings.json 的支持,特别是 Dot Net Core 控制台WinformsWPF 类型。

虽然不是关键,但为了更好地理解,我们将深入研究 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

要获取后缀,我们查看 IHostEnvironmentEnvironmentName 属性的注释。

/// <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,如果存在,则为 DebugRelease 模式使用 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 文件都需要标记为 **内容** 并 **如果较新则复制**,以便在 DebugRelease 模式下使用。
  • 由于所有 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,日志记录为 WarningErrorCritical 级别。

同样适用于 appsettings.json 文件中设置的其他应用程序选项。

实现

我们将创建一个 Console 应用程序来查看上面学到的内容。我们将使用 appsettings 中的示例选项,并将它们映射到一个选项类。代码极简,以保持实现易于理解。

设置

我们需要准备选项。我们将需要一个类来映射 appsettings 文件中的选项部分设置。

  1. 我们的 **设置选项** 类。
    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
  2. 添加到 appsettings 文件。

    现在为不同的配置设置选项。

    1. appsettings.json 文件
      {
        "MySection": {
          "setting1": "default_value_1",
          "setting2": 222
        }
      }
    2. appsettings.Development.json 文件
      {
        "MySection": {
          "setting1": "development_value_1",
          "setting2": 111
        }
      }
    3. appsettings.Production.json 文件
      {
        "MySection": {
          "setting1": "production_value_1",
          "setting2": 333
        }
      }
注释 
* 我们已经设置了强类型访问配置,用于使用 **选项模式**。有关更多信息,请阅读本文: .NET 中的选项模式 | Microsoft Learn

示例控制台应用程序(依赖注入)

现在我们可以从 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 包。

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()

如何测试

我们将逐步配置设置,以了解它们在 DebugRelease 模式下的工作方式。

  1. 仅使用 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
    
  2. 现在包含 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
  3. 现在包含 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
  4. 设置 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** 应用程序中。

参考文献

历史

  • 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 - 添加了关于使用 选项模式 的必要澄清。
© . All rights reserved.