本章包括

  • 从多个配置提供程序加载设置
  • 安全存储敏感设置
  • 使用强类型设置对象
  • 在不同的宿主环境中使用不同的设置

在本书的第1部分中,您学习了ASP.NET Core应用程序启动和运行的基础知识,以及如何使用MVC设计模式创建传统的web应用程序或web API。一旦您开始构建真正的应用程序,您将很快发现您希望在部署时调整各种设置,而不必重新编译应用程序。本章介绍如何使用配置在ASP.NET Core中实现这一点。

我知道。配置听起来很无聊,对吧?但我必须承认,配置模型是ASP.NET Core中我最喜欢的部分之一。它非常容易使用,比以前版本的ASP.NET更优雅。在第11.3节中,您将学习如何从过多的源JSON文件、环境变量和命令行参数加载值,并将它们组合成统一的配置对象。

除此之外,ASP.NET Core还提供了将此配置轻松绑定到强类型选项对象的能力。这些是从配置对象填充的简单POCO类,您可以将其注入到服务中,如第11.4节所示。这使您可以很好地封装应用程序中不同功能的设置。

在本章的最后一节中,您将了解ASP.NET Core托管环境。您通常希望您的应用程序在不同的情况下以不同的方式运行,例如在开发人员计算机上运行时,与将其部署到生产服务器时相比。这些不同的情况称为环境。通过让应用程序知道它在哪个环境中运行,它可以加载不同的配置并相应地改变其行为。

在开始之前,让我们回到基础:什么是配置,为什么需要它,ASP.NET Core如何处理这些需求?

11.1 ASP.NET Core配置模型简介

在本节中,我将简要描述我们所说的配置以及您可以在ASP.NET Core应用程序中使用它做什么。配置是提供给应用程序的一组外部参数,以某种方式控制应用程序的行为。它通常由应用程序将在运行时加载的设置和秘密组成。

定义:设置是更改应用程序行为的任何值。机密是一种特殊类型的设置,包含敏感数据,例如密码、第三方服务的API密钥或连接字符串。

在我们开始之前,一个显而易见的问题是考虑为什么需要应用程序配置,以及需要配置什么样的东西。通常情况下,您应该从应用程序代码中删除任何可以考虑的设置或秘密。这样,您可以在部署时轻松更改这些值,而无需重新编译应用程序。

例如,您可能有一个显示实体店位置的应用程序。您可以为存储商店详细信息的数据库设置连接字符串,还可以设置地图上显示的默认位置、使用的默认缩放级别以及访问GoogleMapsAPI的API键,如图11.1所示。将这些设置和秘密存储在编译代码的外部是一种很好的做法,因为它可以很容易地调整它们,而不必重新编译代码。

图11.1 您可以在配置中存储默认映射位置、缩放级别和映射API密钥,并在运行时加载它们。在配置中和代码中保守API密钥等秘密非常重要。

这还有一个安全方面;您不希望将诸如API密钥或密码之类的秘密值硬编码到代码中,在那里它们可以提交给源代码管理并公开。即使是嵌入在编译的应用程序中的值也可以被提取,因此最好尽可能将它们外部化。

实际上,每个web框架都提供了一种加载配置的机制,而ASP.NET的早期版本也没有什么不同。它使用web.config文件中的<appsettings>元素来存储键值配置对。在运行时,您可以使用

ASP.NET Core配置模型还具有覆盖设置的概念。每个配置提供程序都可以定义自己的设置,也可以覆盖以前提供程序的设置。您将在第11.3节中看到这个非常有用的功能。

ASP.NET Core使将这些定义为字符串的键值对绑定到您在代码中定义的POCO设置类变得简单。这种强类型配置模型使围绕给定特性的设置很容易进行逻辑分组,并且非常适合于单元测试。

在我们深入了解从提供程序加载配置的详细信息之前,我们将后退一步,看看这个过程在HostBuilder中发生的位置。对于使用默认模板构建的ASP.NET Core 5.0应用程序,它始终位于Program.cs中的Host.CreateDefaultBuilder()方法中。

11.2 使用CreateDefaultBuilder配置应用程序

如第2章所示,ASP.NET Core 5.0中的默认模板使用CreateDefaultBuilder方法。这是一个固执己见的助手方法,它为应用程序设置了许多默认值。在本节中,我们将查看该方法的内部,以查看它配置的所有内容以及它们的用途。

清单11.1 使用CreateDefaultBuilder设置配置

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();  //应用程序的入口点创建IHostBuilder,构建IHost,并调用Run。
    }
    //CreateDefaultBuildersetup许多默认值,包括配置。
    public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
}

在第二章中,我对这个方法进行了润色,因为对于简单的应用程序,您几乎不需要更改它。但随着应用程序的增长,如果您想更改应用程序的配置加载方式,您可能会发现需要将其拆分。

此列表显示了CreateDefaultBuilder方法的概述,因此您可以看到HostBuilder是如何构造的。

public static IHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new HostBuilder()  //创建HostBuilder实例
        .UseContentRoot(Directory.GetCurrentDirectory())  //内容根目录定义了可以找到配置文件的目录。
        //配置宿主设置,例如确定宿主环境 
        .ConfigureHostConfiguration(config =>
        {
            // 配置提供程序设置
        })
        .ConfigureAppConfiguration((hostingContext, config) =>  //配置应用程序设置,这是本章的主题
        {
            // 配置提供程序设置
        })
        .ConfigureLogging((hostingContext, logging) =>  //设置日志记录基础结构
        {
            logging.AddConfiguration( hostingContext.Configuration.GetSection("Logging"));
            logging.AddConsole(); 
            logging.AddDebug();
        })
        .UseDefaultServiceProvider((context, options) =>  //配置DI容器,可选地启用验证设置
        {
            var isDevelopment = context.HostingEnvironment
                .IsDevelopment(); options.ValidateScopes = isDevelopment; options.ValidateOnBuild = isDevelopment;
        });
 
    return builder;  //通过在调用Build()之前调用额外的方法,返回HostBuilder以进行进一步配置
}

在HostBuilder上调用的第一个方法是UseContentRoot。这会告诉应用程序在哪个目录中可以找到以后需要的任何配置或视图文件。这通常是运行应用程序的文件夹,因此调用GetCurrentDirectory。

提示:ContentRoot不是您存储浏览器可以直接访问的静态文件的地方——这是WebRoot,通常是wwwroot。

ConfigureHostingConfiguration()方法是应用程序确定其当前运行的HostingEnvironment的方法。默认情况下,框架会查找环境变量和命令行参数,以确定它是在开发环境还是生产环境中运行。您将在第11.5节中了解有关托管环境的更多信息。

ConfigureLogging用于指定应用程序的日志记录设置。我们将在第17章详细讨论日志记录;现在,只要知道CreateDefaultBuilder为您设置了这一点就足够了。

CreateDefaultBuilder中的最后一个方法调用UseDefaultServiceProvider将应用程序配置为使用内置DI容器。它还基于当前HostingEnvironment设置ValidateScopes和ValidateOnBuild选项。在开发环境中运行应用程序时,应用程序将自动检查捕获的依赖关系,这是您在第10章中了解的。

ConfigureAppConfiguration()方法是第11.3节的重点。无论是JSON文件、环境变量还是命令行参数,都可以在这里加载应用程序的设置和秘密。在下一节中,您将看到如何使用此方法使用ASP.NET Core ConfigurationBuilder从各种配置提供程序加载配置值。

11.3 为应用程序构建配置对象

在本节中,我们将进入配置系统的核心部分。您将学习如何从多个源加载设置,如何在ASP.NET Core中内部存储设置,以及设置如何覆盖其他值以提供“层”配置。您还将学习如何安全地存储机密,同时确保在运行应用程序时机密仍然可用。
在第11.2节中,您看到了如何使用CreateDefaultBuilder方法创建IHostBuilder的实例。IHostBuilder负责设置应用程序的许多功能,包括ConfigureAppConfiguration方法中的配置系统。此方法传递给Configuration-Builder的一个实例,该实例用于定义应用程序的配置。

ASP.NET Core配置模型以两个主要结构为中心:ConfigurationBuilder和IConfiguration。

注意:ConfigurationBuilder描述了如何构建应用程序的最终配置表示,IConfiguration本身保存配置值。

通过向ConfigureAppConfiguration中的ConfigurationBuilder添加多个IConfigurationProvider来描述配置设置。这些描述了如何从特定源加载键值对;例如,JSON文件或环境变量,如图11.2所示。在ConfigurationBuilder上调用Build()将查询这些提供程序中的每一个,以获取它们所包含的值,从而创建IConfigurationRoot实例。

注意:调用Build()将创建一个IConfigurationRoot实例,该实例实现IConfiguration。您通常会在代码中使用IConfiguration接口。

图11.2 使用ConfigurationBuilder创建IConfiguration实例。配置提供程序通过扩展方法添加到生成器中。调用Build()查询每个提供程序以创建IConfigurationRoot,该提供程序实现IConfiguration。

如果这些不符合您的要求,您可以在GitHub和NuGet上找到一整套替代方案,创建自己的定制提供商并不困难。例如,您可以使用官方Azure密钥库提供程序NuGet package1或我编写的YAML文件提供程序。2

在许多情况下,默认提供程序就足够了。特别是,大多数模板以appsettings.json文件开头,该文件包含各种设置,具体取决于您选择的模板。下面的列表显示了ASP.NET Core 5.0 Web App模板在未经身份验证的情况下生成的默认文件。

清单11.3 ASP.NET Core Web模板创建的默认appsettings.json文件

{
    "Logging": {
        "LogLevel": {
            "Default": "Information", "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*"
}

ASP.NET Core附带配置提供程序,用于从公共位置加载数据:

  • JSON文件
  • XML文件
  • 环境变量
  • 命令行参数
  • INI文件

如您所见,此文件主要包含控制日志记录的设置,但您也可以在此处添加应用程序的其他配置。

警告:不要在此文件中存储敏感值,例如密码、API密钥或连接字符串。您将在第11.3.3节中了解如何安全存储这些文件。

添加自己的配置值涉及向JSON添加键值对。通过为相关设置创建一个基本对象来“命名”设置是一个好主意,如这里所示的MapSettings对象。

清单11.4 向appsettings.json文件添加其他配置值

{
    "Logging": {
        "LogLevel": {
            "Default": "Information", "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*",
    "MapSettings":   //将所有配置嵌套在MapSettings键下。
    { 
        "DefaultZoomLevel": 9,  //值可以是JSON文件中的数字,但在读取时将转换为字符串。 
        "DefaultLocation": {   ////您可以创建深度嵌套的结构,以更好地组织配置值。
            "latitude": 50.500,
            "longitude": -4.000
        }
    }
}

我已经将新配置嵌套在MapSettings父项中;这将创建一个“部分”,稍后在将值绑定到POCO对象时将非常有用。我还将纬度和经度键嵌套在DefaultLocation键下。你可以创建任何你喜欢的价值结构;配置提供程序将很好地读取它们。此外,您可以将它们存储为任何数据类型(在本例中为数字),但请注意,提供程序将在内部以字符串形式读取和存储它们。

提示:应用程序中的配置键不区分大小写,因此在从密钥区分大小写的提供程序加载时,请记住这一点。

现在您已经有了配置文件,现在是应用程序使用ConfigurationBuilder加载它的时候了。为此,我们将返回HostBuilder在Program.cs中公开的ConfigureAppConfiguration()方法。

11.3.1 在Program.cs中添加配置提供程序

ASP.NET Core中的默认模板使用CreateDefaultBuilder助手方法为应用程序引导HostBuilder,如第11.2节所示。作为此配置的一部分,CreateDefaultBuilder方法调用ConfigureAppConfiguration并设置许多默认配置提供程序,我们将在本

章中详细介绍这些提供程序:

  • JSON文件提供程序——从名为appsettings.JSON的可选JSON文件加载设置。它还从名为appsettings.environment.JSON的特定于环境的可选JSON文档加载设置。我将在第11.5节中演示如何使用特定于环境文件。
  • 用户机密——加载在开发期间安全存储的机密。
  • 环境变量——将环境变量作为配置变量加载。这些非常适合在生产中存储秘密。
  • 命令行参数——在运行应用程序时使用作为参数传递的值。

使用默认生成器将您绑定到此默认集,但默认生成器是可选的。如果要使用不同的配置提供程序,可以创建自己的HostBuilder实例。如果采用这种方法,则需要设置CreateHostBuilder所做的每一件事:日志记录、托管配置、服务提供商配置以及应用程序配置。

另一种方法是通过向ConfigureAppConfiguration添加额外的调用来添加额外的配置提供程序,如以下列表所示。这允许您在CreateHostBuilder添加的提供程序之上添加额外的提供程序。在下面的列表中,您显式清除默认提供程序,这允许您完全自定义从何处加载配置,而无需替换CreateHostBuilder为日志等添加的默认值。

清单11.5 使用自定义HostBuilder加载appsettings.json

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
 
    public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration(AddAppConfiguration)  //将配置设置功能添加到HostBuilder
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
    //HostBuilder提供托管上下文和ConfigurationBuilder的实例。
    public static void AddAppConfiguration( HostBuilderContext hostingContext, IConfigurationBuilder config)
    {
        config.Sources.Clear();   //清除CreateDefaultBuilder中默认配置的提供程序
        config.AddJsonFile("appsettings.json", optional: true);  //添加JSON配置提供程序,提供配置文件的文件名
    }
}

提示:在清单11.5中,我将配置提取到一个静态助手方法Add-AppConfiguration,但您也可以将其作为lambda方法内联提供。

HostBuilder在调用ConfigureAppConfiguration方法之前创建ConfigurationBuilder实例。您需要做的就是为应用程序添加配置提供程序。

在本例中,通过调用AddJsonFile扩展方法并提供文件名,您添加了一个JSON配置提供程序。您还将optional的值设置为true。这告诉配置提供程序跳过在运行时找不到的文件,而不是抛出FileNotFoundException。注意,此时扩展方法只注册提供程序;它还没有尝试加载文件。

就这样!HostBuilder实例负责调用Build(),它生成IConfiguration,表示配置对象。然后将其注册到DI容器中,这样您就可以将其注入到类中。您通常会将其注入Startup类的构造函数中,因此可以在Configure和ConfigureServices方法中使用它:

public class Startup
{
  public Startup(IConfiguration config)
  {
    Configuration = config;
  }
  public IConfiguration Configuration { get; }
}

注意:ConfigurationBuilder创建一个IConfigurationRoot实例,该实例实现IConfiguration。这在DI容器中注册为IConfiguration,而不是IConfigurationRoot。IConfiguration是少数可以注入Startup构造函数的东西之一。

此时,在Startup构造函数的末尾,您有一个完全加载的配置对象。但你能用它做什么?IConfiguration将配置存储为一组键值字符串对。您可以使用标准字典语法通过其键访问任何值。例如,您可以使用

var zoomLevel = Configuration["MapSettings:DefaultZoomLevel"];

以检索应用程序的配置缩放级别。注意,我使用冒号(:)来指定一个单独的部分。类似地,要检索纬度键,可以使用

Configuration["MapSettings:DefaultLocation:Latitude"];

注意:如果请求的配置密钥不存在,您将得到一个空值。

您还可以使用GetSection(section)方法获取配置的整个部分,该方法返回实现IConfiguration的IConfigurationSection。这将获取一块配置并重置命名空间。获取纬度键的另一种方法是

Configuration.GetSection("MapSettings")["DefaultLocation:Latitude"];

在定义应用程序时,在ConfigureServices和Configure Startup方法中访问这样的设置值非常有用。例如,当设置应用程序以连接到数据库时,通常会从Configuration对象加载一个连接字符串(在下一章中,当我们查看实体框架核心时,您将看到一个具体的例子)。

如果需要从Startup以外的类访问这样的配置对象,可以使用DI将其作为服务构造函数中的依赖项。但使用这样的字符串键访问配置并不是特别方便;您应该尝试使用强类型配置,如第11.4节所示。

到目前为止,从JSON文件加载设置可能感觉有点复杂,而且很普通,我会向您保证,确实如此。ASP.NET Core配置系统的亮点是当您有多个提供程序时。

11.3.2 使用多个提供程序覆盖配置值

您已经看到ASP.NET Core使用Builder模式来构造配置对象,但到目前为止,您只配置了一个提供程序。在添加提供程序时,重要的是要考虑添加它们的顺序,因为这定义了配置值添加到基础字典的顺序。来自较新提供程序的配置值将使用来自较早提供程序的相同密钥覆盖值。

注意:这需要重复:向ConfigurationBuilder添加配置提供程序的顺序很重要。后期配置提供程序可以覆盖早期提供程序的值。

将配置提供程序视为将配置值的“层”添加到堆栈中,其中每个层可能与下面的一些或所有层重叠,如图11.3所示。当调用Build()时,ConfigurationBuilder将这些层折叠为一层,以创建存储在IConfiguration中的最后一组配置值。

图11.3 每个配置提供程序都向ConfigurationBuilder添加一个值“层”。调用Build()会折叠该配置。稍后的提供程序将使用与先前提供程序相同的密钥覆盖配置值。

更新代码以从三个不同的配置提供程序(两个JSON提供程序和一个环境变量提供程序)加载配置,方法是将它们添加到ConfigurationBuilder中。为了简洁起见,我只在清单11.6中展示了AddAppConfiguration方法。

清单11.6 从Startup.cs中的多个提供程序加载

public class Program
{
/* 附加程序配置 */ 
    public static void AddAppConfiguration(
    HostBuilderContext hostingContext, IConfigurationBuilder config)
    {
        config.Sources.Clear(); 
        config
            .AddJsonFile("sharedSettings.json", optional: true)  //在appsettings.JSON文件之前从不同的JSON配置文件加载配置
            .AddJsonFile("appsettings.json", optional: true)
            .AddEnvironmentVariables();  //将计算机的环境变量添加为配置提供程序
    }	
}	

这种分层设计在很多方面都很有用。从根本上讲,它允许您将来自多个不同源的配置值聚合到单个内聚对象中。要将其固定到位,请考虑图11.4中的配置值。

图11.4 最终的IConfiguration包括每个提供程序的值。appsettings.json和环境变量都包含MyAppConnString键。当稍后添加环境变量时,将使用该配置值。

每个提供程序中的大多数设置都是唯一的,并添加到最终的IConfiguration中。但“MyAppConnString”键同时出现在appsettings.json和环境变量中。因为环境变量提供程序添加在JSON提供程序之后,所以IConfiguration中使用环境变量配置值。

从多个提供程序中整理配置的能力本身就是一个方便的特点,但在处理敏感的配置值(如连接字符串和密码)时,这种设计尤其有用。下一节将介绍如何在开发机器和生产服务器上本地处理此问题。

11.3.3 安全存储配置机密

一旦你构建了一个非常重要的应用程序,你就会发现你需要在某个地方存储一些敏感数据作为设置。例如,这可以是远程服务的密码、连接字符串或API密钥。

将这些值存储在appsettings.json中通常是一个坏主意,因为您不应该将机密提交给源代码管理;人们提交给GitHub的秘密API密钥数量太可怕了!相反,最好将这些值存储在项目文件夹之外,这样它们就不会被意外提交。

您可以通过几种方式实现这一点,但最简单和最常用的方法是在生产服务器上使用环境变量作为机密,并在本地使用用户机密。

这两种方法都不是真正安全的,因为它们不以加密格式存储值。如果您的机器受到攻击,攻击者将能够读取存储的值,因为它们以明文形式存储。它们旨在帮助您避免向源代码管理提交机密。

提示:Azure Key Vault3是一个安全的替代方案,因为它存储在Azure中加密的值。但您仍需要使用以下方法来存储Azure Key Value连接详细信息。另一个流行的选项是Hashicorp的Vault(www.vaultproject.io/),它可以在您的场所或云中运行。

无论使用哪种方法存储应用程序机密,如果可能的话,请确保没有将其存储在源代码管理中。即使是私有存储库也不可能永远保持私有,因此最好谨慎行事。

在生产环境变量中存储秘密

您可以使用AddEnvironmentVariables扩展方法添加环境变量配置提供程序,如清单11.6所示。这会将计算机上的所有环境变量作为键值对添加到配置对象中。

注意:环境变量提供程序默认添加到Create-DefaultBuilder中,如第11.2节所示。

通过使用冒号(:)或双下划线()来划分一个部分,可以在环境变量中创建与JSON文件中通常看到的相同的分层部分;例如,MapSettings:MaxNumberOfPoints或MapSettings MaxNumberOfPoint。

提示:某些环境(如Linux)不允许在环境变量中使用冒号。您必须在这些环境中使用双下划线方法。环境变量中的双下划线将在导入IConfiguration对象时转换为冒号。从应用程序中的IConfiguration检索值时,应始终使用冒号。

当您将应用程序发布到独立环境(如专用服务器、Azure或Docker容器)时,环境变量方法特别有用。您可以在生产机器或Docker容器上设置环境变量,提供者将在运行时读取这些变量,覆盖appsettings.json文件中指定的默认值。

对于开发机器,环境变量不太有用,因为所有应用程序都使用相同的值。例如,如果设置ConnectionStringsDefaultConnection环境变量,则会为本地运行的每个应用程序添加该变量。这听起来更像是一个麻烦而不是一个好处!

对于开发方案,可以使用用户机密管理器。这有效地添加了每个应用程序的环境变量,因此您可以为每个应用程序设置不同的设置,但将它们存储在与应用程序本身不同的位置。

在开发中使用用户机密管理器存储机密

用户秘密背后的想法是简化在应用程序项目树之外存储每个应用程序的秘密。这类似于环境变量,但您为每个应用程序使用一个唯一的密钥来隔离秘密。

警告:机密未加密,因此不应被视为安全的。尽管如此,将它们存储在项目文件夹中是一种改进。

设置User Secrets比使用环境变量需要花费更多的精力,因为您需要配置一个工具来读取和写入它们,添加User Secretes配置提供程序,并为应用程序定义一个唯一的密钥:

  1. ASP.NET Core默认包含用户机密提供程序。.NETSDK还包括一个用于从命令行处理机密的全局工具。
  2. 如果您使用的是Visual Studio,请右键单击项目并选择“管理用户机密”。这将打开secrets.json文件的编辑器,您可以在其中存储键值对,就像它是一个appsettings.json文件一样,如图11.5所示。
    向.csproj文件添加唯一标识符。当您单击“管理用户机密”时,Visual Studio会自动执行此操作,但如果您使用的是命令行,则需要自己添加。通常,您会使用唯一的ID,如GUID:
    <PropertyGroup>
    <UserSecretsId>96eb2a39-1ef9-4d8e-8b20-8e8bd14038aa</UserSecretsId>
    </PropertyGroup>

    图11.5 选择Manage User Secrets(管理用户机密)以打开User Secretes(用户机密)应用程序的编辑器。在本地开发应用程序时,可以使用此文件存储机密。这些文件存储在项目文件夹之外,因此不会意外提交到源代码管理。
  3. 如果您没有使用Visual Studio,可以使用命令行添加用户机密
    dotnet user-secrets set "MapSettings:GoogleMapsApiKey" F5RJT9GFHKR7
  4. 或者您可以使用自己喜欢的编辑器直接编辑secret.json文件。此文件的确切位置取决于您的操作系统,可能会有所不同。查看文档以了解详细信息。

呸,这是一个很大的设置,如果你正在定制HostBuilder,你还没有完成!您需要更新应用程序,以便在运行时使用ConfigureAppConfiguration方法中的AddUserSecrets扩展方法加载User Secrets:

if(env.IsDevelopment())
{
  configBuilder.AddUserSecrets<Startup>();
}

注意:您只应在开发中使用User Secrets提供程序,而不应在生产中使用,因此在前面的代码段中,您有条件地将该提供程序添加到ConfigurationBuilder中。在生产环境中,您应该使用环境变量或Azure密钥库,如前所述。如果使用Host.CreateDefaultBuilder(),则默认情况下会正确配置。

此方法有许多重载,但最简单的是一个泛型方法,您可以调用它将应用程序的Startup类作为泛型参数传递。用户机密提供程序需要读取您(或Visual Studio)添加到.csproj文件中的UserSecretsId属性。Startup类充当一个简单的标记,指示哪个程序集包含此属性。

注意:如果您感兴趣,User Secrets包将使用.csproj文件中的UserSecretsId属性来生成程序集级UserSecreteIdAttribute。然后,提供程序在运行时读取该属性以确定应用程序的UserSecretsId,从而生成secrets.json文件的路径。

在开发过程中,您可以将机密安全地存储在项目文件夹之外。这可能看起来有些过分,但如果您有任何远程敏感的东西需要加载到配置中,我强烈建议您使用环境变量或用户机密。

现在几乎是将配置提供程序抛在身后的时候了,但在我们这样做之前,我想向您展示ASP.NET Core配置系统的派对技巧:动态重新加载文件。

11.3.4 更改配置值时重新加载配置值

除了安全性之外,使用配置和设置的优点之一是每次需要调整值时不必重新编译应用程序。在ASP.NET的早期版本中,通过编辑web.config更改设置将导致应用程序必须重新启动。这比必须重新编译要好得多,但等待应用程序启动后才能处理请求有点麻烦。

在ASP.NET Core中,您终于能够编辑文件并自动更新应用程序的配置,而无需重新编译或重新启动。

一个经常被引用的场景是,当您尝试调试生产中的应用程序时,您可能会发现这很有用。您通常将日志记录配置为以下级别之一:

  • Error
  • Warning
  • Information
  • Debug

这些设置中的每一个都比上一个更详细,但它也提供了更多的上下文。默认情况下,您可以将应用程序配置为只记录生产中的警告和错误级别日志,这样就不会生成太多多余的日志条目。相反,如果您试图调试问题,则需要尽可能多的信息,因此可能需要使用调试日志级别。

能够在运行时更改配置意味着您可以在遇到问题时轻松打开额外的日志,然后通过编辑appsettings.json文件将其切换回来。

注意:重新加载通常仅适用于基于文件的配置提供程序,而不是环境变量或用户机密提供程序。

当您将任何基于文件的提供程序添加到ConfigurationBuilder时,可以启用重新加载配置文件。Add*文件扩展名方法包含带有reloadOnChange参数的重载。如果设置为true,则应用程序将监视文件系统中文件的更改,并在需要时触发IConfiguration的完全重建。此列表显示如何将配置重新加载添加到AddAppConfiguration方法中加载的appsettings.json文件。

清单11.7 文件更改时重新加载appsettings.json

public class Program
{
    /* 附加程序配置 */
    public static void AddAppConfiguration( HostBuilderContext hostingContext, IConfigurationBuilder config)
    {
        //如果appsettings.json文件发生更改,将重新生成IConfiguration。
        config.AddJsonFile( "appsettings.json", optional: true reloadOnChange: true);
    }
}

安装好后,您对文件所做的任何更改都将在IConfiguration中进行镜像。但正如我在本章开头所说,IConfiguration不是在应用程序中传递设置的首选方式。相反,正如您将在下一节中看到的,您应该支持强类型的POCO对象。

11.4 在选项模式中使用强类型设置

在本节中,您将了解强类型配置和选项模式。这是访问ASP.NET Core中配置的首选方式。通过使用强类型配置,您可以避免在访问配置时出现拼写错误问题。它还使类更容易测试,因为您可以使用简单的POCO对象进行配置,而不是依赖于IConfiguration抽象。

到目前为止,我展示的大多数示例都是关于如何将值输入IConfiguration,而不是如何使用它们。您已经看到,您可以使用Configuration[“key”]字典语法访问键,但使用这样的字符串键会感觉很混乱,容易出现拼写错误。

相反,ASP.NET Core提倡使用强类型设置。这些是您定义和创建的POCO对象,它们代表一小部分设置,范围仅限于应用程序中的一个功能。

以下列表显示了商店定位器组件的设置和自定义应用程序主页的显示设置。它们被分成两个不同的对象,分别具有“MapSettings”和“AppDisplaySettings”键,对应于它们影响的应用程序的不同区域。

清单11.8 在appsettings.json中将设置分隔成不同的对象

{
    //与应用程序的商店定位器部分相关的设置
    "MapSettings": { "DefaultZoomLevel": 6, 
        "DefaultLocation": {
        "latitude": 50.500,
        "longitude": -4.000
        }
    },
    //与显示应用程序相关的常规设置
    "AppDisplaySettings": {
        "Title": "Acme Store Locator", "ShowCopyright": true
    }
}

使主页设置在Index.chtml Razor page中可用的最简单方法是将IConfiguration注入PageModel,并使用字典语法访问值:

public class IndexModel : PageModel
{
  public IndexModel(IConfiguration config)
  {
    var title = config["HomePageSettings:Title"]; 
    var showCopyright = bool.Parse(
      config["HomePageSettings:ShowCopyright"]);
  }
}

但你不想这样做;我不喜欢太多的字符串!还有那个嘘声,解析?大笑!相反,您可以使用自定义强类型对象,并具有所有类型安全性和IntelliSense优点。

public class IndexModel: PageModel
{
    public IndexModel(IOptions<AppDisplaySettings> options)  //您可以使用IOptions<>包装器接口注入强类型选项类。
    {
        AppDisplaySettings settings = options.Value;   //Value属性公开POCO设置对象。
        var title = settings.Title;  //设置对象包含在运行时绑定到配置值的属性。
        bool showCopyright = settings.ShowCopyright;  //活页夹还可以将字符串值直接转换为基元类型。
    }
} 

ASP.NET Core配置系统包括一个绑定器,它可以获取配置值的集合,并将它们绑定到一个强类型的对象,称为选项类。这类似于第6章中的模型绑定概念,其中请求值被绑定到POCO绑定模型类。

本节介绍如何将配置值绑定到POCO选项类,以及如何确保在基础配置值更改时重新加载。我们还将查看您可以绑定的不同类型的对象。

11.4.1 IOptions界面介绍

ASP.NET Core引入了强类型设置,以使配置代码遵循单一责任原则,并允许将配置类作为显式依赖项注入。这样的设置也使测试更容易;您可以创建POCO选项类的实例,而不必创建IConfiguration的实例来测试服务。

例如,上一个示例中显示的AppDisplaySettings类可以很简单,只显示与主页相关的值:

public class AppDisplaySettings
{
  public string Title { get; set; } 
  public bool ShowCopyright { get; set; }
}

您的选项类需要是非抽象的,并且具有公共的无参数构造函数,才有资格进行绑定。绑定器将设置与配置值匹配的任何公共属性,您很快就会看到。

提示:您不限于string和bool等原始类型;也可以使用嵌套的复杂类型。选项系统会将节绑定到复合属性。有关示例,请参阅相关的源代码。

为了帮助将配置值绑定到自定义POCO选项类,ASP.NET Core引入了IOptions<T>接口。这是一个具有单个属性Value的简单接口,其中包含运行时配置的POCO选项类。选项类在Startup的ConfigureServices部分中设置,如下所示。

清单11.10 使用Startup.cs中的Configure<T>配置选项类

public IConfiguration Configuration { get; }
//将MapSettings部分绑定到POCO选项类MapSettings
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MapSettings>( Configuration.GetSection("MapSettings"));
    //Binds the AppDisplaySettings section to the POCO options class AppDisplaySettings
    services.Configure<AppDisplaySettings>( Configuration.GetSection("AppDisplaySettings"));
}

提示:您不必像我在清单11.10中那样对节和类使用相同的名称;这只是我喜欢遵循的惯例。使用此约定,您可以使用nameof()运算符来进一步减少键入错误的机会,例如通过调用GetSection(nameof(MapSettings))。

每次调用Configure<T>都会在内部设置以下一系列操作:

  1. 创建ConfigureOptions<T>的实例,该实例指示IOptions<T>应基于配置进行配置。
    如果多次调用Configure<T>,则将使用多个ConfigureOptions<T>对象,所有这些对象都可以应用于创建最终对象,这与从多个层构建IConfiguration的方式大致相同。
  2. 每个ConfigureOptions<T>实例都将IConfiguration的一部分绑定到T POCO类的一个实例。这将基于提供的ConfigurationSection中的键设置options类的任何公共属性。
    请记住,节名(清单11.10中的“MapSettings”)可以具有任何值;它不必与选项类的名称匹配。
  3.  IOptions<T>接口在DI容器中注册为单例,最终绑定的POCO对象位于Value属性中。

最后一步允许您通过注入IOptions<T>将选项类注入控制器和服务,如您所见。这为您提供了对配置值的封装、强类型访问。没有更多的魔术串了,呜呜!

警告:如果您忘记调用Configure<T>并将IOptions<T>注入到服务中,您将不会看到任何错误,但T选项类不会绑定到任何对象,并且其属性中只有默认值。

当您第一次请求IOptions<T>时,就会将T选项类绑定到ConfigurationSection。该对象在DI容器中注册为单例,因此只绑定一次。

这个设置有一个缺点:当使用IOptions<t>时,不能使用11.3.4节中描述的reloadOnChange参数来重新加载强类型选项类。如果编辑appsettings.json文件,IConfiguration仍将重新加载,但不会传播到选项类。

如果这看起来像是一个倒退,甚至是一个交易破坏者,那么不要担心。IOptions<T>有一个表亲IOptions小睡<T>,适合这样的场合。

11.4.2 使用IOptionsSnapshot重新加载强类型选项

在上一节中,您使用了IOptions<T>来提供对配置的强类型访问。这为特定服务的设置提供了一个很好的封装,但有一个特定的缺点:选项类永远不会更改,即使您修改了加载它的底层配置文件,例如appsettings.json。

这通常不是问题(您实际上不应该修改实时生产服务器上的文件),但如果您需要此功能,可以使用IOptionsSnapshot<t>界面。

从概念上讲,IOptionsSnapshot<T>与IOptions<T>相同,因为它是配置部分的强类型表示。不同之处在于,在使用POCO选项对象时,创建POCO选项的时间和频率。

  • IOptions<T>——在第一次需要时创建一次实例。它始终包含首次创建对象实例时的配置。
  • IOptionsSnapshot<T>——如果自上次创建实例以来基础配置已更改,则在需要时创建新实例。

IOptionsSnapshot<T>与IOptions<T>同时自动为您的选项类设置,因此您可以以完全相同的方式在服务中使用它。此列表显示如何更新IndexModel主页,以便始终在强类型的AppDisplaySettings选项类中获取最新的配置值。

清单11.11 使用IOptionsSnapshot注入可重新加载的选项

public class IndexModel: PageModel
{
    public IndexModel(
        IOptionsSnapshot<AppDisplaySettings> options)  //如果基础配置值更改,IOptionsSnapshot<T>将更新。
    {
        //Value属性公开POCO设置对象,与IOptions<T>相同。
        AppDisplaySettings settings = options.Value;   //设置对象将在某个时刻与配置值匹配,而不是在第一次运行时。
        var title = settings.Title;
    }
}

每当您编辑设置文件并重新加载IConfiguration时,IOptions-Snapshot<AppDisplaySettings>将被重建。将使用新的配置值创建一个新的AppDisplaySettings对象,并将用于所有未来的依赖注入。当然,在您再次编辑文件之前!就这么简单;更新代码以在需要时使用IOptionsSnapshot<T>而不是IOptions<T>。

使用选项模式时的一个重要考虑是POCO选项类本身的设计。这些通常是属性的简单集合,但要记住一些事情,这样您就不会陷入调试绑定似乎不起作用的困境。

11.4.3 为自动绑定设计选项类

我已经谈到了POCO类与IOptions<T>绑定器一起工作的一些要求,但有一些规则需要记住。

第一个关键点是绑定器将使用反射创建选项类的实例,因此POCO选项类需要

  • 非抽象的
  • 具有默认(公共无参数)构造函数

如果您的类满足这两点,绑定器将循环遍历类上的所有属性,并绑定它可以绑定的任何属性。从广义上讲,绑定器可以绑定

  • 是公共的
  • 有一个getter——绑定器不会只写set属性
  • 具有setter,或者对于复杂类型,具有非null值
  • 不是索引器

下面的列表显示了一个包含大量不同类型属性的扩展选项类,其中一些属性可以绑定,有些属性不能绑定。

清单11.12 包含绑定和非绑定属性的选项类

public class TestOptions
{
    //绑定器可以使用默认值绑定简单和复杂的对象类型以及只读属性。
    public string String { get; set; } 
    public int Integer { get; set; } 
    public SubClass Object { get; set; }
    public SubClass ReadOnly { get; } = new SubClass();
    //绑定器还将绑定集合,包括接口;字典必须有字符串键。
    public Dictionary<string, SubClass> Dictionary { get; set; } 
    public List<SubClass> List { get; set; }
    public IDictionary<string, SubClass> IDictionary { get; set; } 
    public IEnumerable<SubClass> IEnumerable { get; set; }
    public ICollection<SubClass> IEnumerable { get; } = new List<SubClass>();
    //绑定器不能绑定非公共、只读、空只读或索引器属性。
    internal string NotPublic { get; set; }
    public SubClass SetOnly { set => _setOnly = value; } 
    public SubClass NullReadOnly { get; } = null;
    public SubClass NullPrivateSetter { get; private set; } = null; 
    public SubClass this[int i] {
        get => _indexerList[i];
        set => _indexerList[i] = value;
    }
    //无法绑定这些集合属性。
    public List<SubClass> NullList { get; }
    public Dictionary<int, SubClass> IntegerKeys { get; set; } 
    public IEnumerable<SubClass> ReadOnlyEnumerable { get; } = new List<SubClass>();
    //用于实现SetOnly和Indexer属性的支持字段-不直接绑定 
    public SubClass _setOnly = null;
    private readonly List<SubClass> _indexerList = new List<SubClass>(); public class SubClass
    {
        public string Value { get; set; }
    }
}

如清单所示,绑定器通常支持集合——包括实现和接口。如果集合属性已经初始化,它将使用该属性,但活页夹也可以为它们创建支持字段。如果您的属性实现以下任何一个类,绑定器将创建一个适当类型的List<>作为支持对象:

  • IReadOnlyList<>
  • IReadOnlyCollection<>
  • ICollection<>
  • IEnumerable<>

警告:无法绑定到已初始化的IEnumerable<>属性,因为基础类型不公开Add函数。如果将IEnumerable<>的初始值保留为空,则可以绑定到它。

类似地,绑定器将创建一个Dictionary<,>作为具有字典接口的属性的支持字段,只要它们使用字符串键:

  • IDictionary<string,>
  • IReadOnlyDictionary<string,>

警告:不能使用非字符串键(如int)绑定字典。有关绑定集合类型的示例,请参阅本书的相关源代码。

显然这里有很多细微差别,但如果你坚持前面例子中的简单例子,你会没事的。确保检查JSON文件中的拼写错误!

提示:选项模式最常用于将POCO类绑定到配置,但也可以通过向configure函数提供lambda来在代码中配置强类型设置类;例如,服务。配置<TestOptions>(opt=>opt.Value=true)。

选项模式在ASP.NET Core中使用,但并非每个人都是粉丝。在下一节中,您将看到如何在没有Options模式的情况下使用强类型设置和配置绑定器。

11.4.4 在没有IOptions接口的情况下绑定强类型设置

IOptions接口在ASP.NET Core中非常规范——它由核心ASP.NET Core库使用,并具有各种方便功能,用于绑定强类型设置,正如您已经看到的那样。

然而,在许多情况下,IOptions接口并不能为强类型设置对象的使用者带来很多好处。服务必须依赖于IOptions接口,然后通过调用IOptions<T>.Value立即提取“真实”对象。如果您正在构建一个与ASP.NET Core没有内在联系的可重用库,这可能特别令人讨厌,因为您必须在所有公共API中公开IOptions>接口。

幸运的是,将IConfiguration对象映射到强类型设置对象的配置绑定器并不固有地与IOptions绑定。清单11.13显示了如何手动将强类型设置对象绑定到配置节并将其注册到DI容器。

清单11.13 在Startup.cs中配置不带IOptions的强类型设置

public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
    var settings = new MapSettings ();  //创建MapSettings对象的新实例。
    Configuration.GetSection("MapSettings").Bind(settings);   //将IConfiguration中的MapSettings部分绑定到设置对象。
    services.AddSingleton(settings);  //将设置对象注册为单例。
}

现在,您可以将MapSettings对象直接注入到服务中,而无需使用IOptions<MapSettings>所需的额外仪式。

public class MyMappingController
{
  private readonly MapSettings _settings;
  public MyMappingController(MapSettings settings)
  {
    _settings = settings;
  }
}

如果使用这种方法,您将无法在不做进一步工作的情况下重新加载强类型设置,也无法从IOptions的一些更高级用法中获益,但在大多数情况下,这不是一个大问题。一般来说,我是这种方法的粉丝,但一如既往,在全心全意地采用它之前,先考虑一下你正在失去什么。

提示:在第19章中,我展示了一个这样的场景,即使用DI容器中的服务配置IOptions对象。有关其他高级场景,请参阅Microsoft的“ASP.NET Core中的选项模式”文档,http://mng.bz/DR7y,或查看我博客上的各种IOptions帖子,例如:http://mng.bz/l1Aj.

这就结束了关于强类型设置的本节。在下一节中,我们将介绍如何在运行时根据应用程序运行的环境动态更改设置。

11.5 为多个环境配置应用程序

在本节中,您将了解ASP.NET Core中的托管环境。您将学习如何设置和确定应用程序正在哪个环境中运行,以及如何根据环境更改使用的配置值。例如,这使您可以轻松地在生产和开发中的不同配置值集之间进行切换。

任何能够投入生产的应用程序都可能必须在多个环境中运行。例如,如果您正在构建具有数据库访问功能的应用程序,那么您的计算机上可能会运行一个用于开发的小型数据库。在生产环境中,您将在其他地方的服务器上运行一个完全不同的数据库。

另一个常见的要求是根据应用程序的运行位置,有不同的日志记录量。在开发过程中,生成大量日志是很好的,因为它有助于调试,但一旦进入生产阶段,过多的日志记录可能会让人不知所措。您将希望记录警告和错误,可能是信息级别的日志,但绝对不是调试级别的日志!

要处理这些需求,您需要确保应用程序根据运行环境加载不同的配置值:在生产环境中加载生产数据库连接字符串,依此类推。您需要考虑三个方面:

  • 你的应用程序如何识别它在哪个环境中运行?
  • 如何根据当前环境加载不同的配置值?
  • 如何更改特定机器的环境?

本节依次解决了这些问题,因此您可以轻松地将开发机器与生产服务器区分开来,并采取相应的行动。

11.5.1 确定托管环境

正如您在第11.2节中看到的,Host-Builder上的ConfigureHostingConfiguration方法是定义应用程序如何计算宿主环境的地方。默认情况下,CreateDefaultBuilder使用一个环境变量来标识当前环境,这也许不足为奇。HostBuilder查找名为ASP.NET Core_ENVIRONMENT的神奇环境变量,并使用它创建IHostEnvironment对象。

注意:您可以使用DOTNET_ENVIRONMENT或ASP.NET Core_ENVIRONMENT环境变量。如果两者都已设置,ASP.NET Core_值将覆盖DOTNET_值。我在本书中使用ASP.NET Core_版本。

IHostEnvironment界面公开了许多有关应用程序运行上下文的有用属性。其中一些您已经看到过,例如Content-RootPath,它指向包含应用程序内容文件的文件夹;例如,appsettings.json文件。您在此感兴趣的属性是EnvironmentName。

IHostEnvironment.EnvironmentName属性设置为ASP.NET Core_ENVIRONMENT环境变量的值,因此它可以是任何值,但在大多数情况下,您应该坚持使用三个常用值:

  • "Development"
  • "Staging"
  • "Production"

ASP.NET Core包括几个用于处理这三个值的助手方法,因此如果您坚持使用它们,您将更轻松。特别是,无论何时测试应用程序是否在特定环境中运行,都应使用以下扩展方法之一:

  • IsDevelopment()
  • IsStaging()
  • IsProduction()
  • IsEnvironment(string environmentName)

这些方法都确保对环境变量进行不区分大小写的检查,因此如果不将环境变量值大写,在运行时不会出现任何不稳定的错误。

提示:在可能的情况下,使用IHostEnvironment扩展方法与EnvironmentValue进行直接字符串比较,因为它们提供了不区分大小写的匹配。

IHostEnvironment除了公开当前环境的细节之外,不会做任何其他事情,但您可以以各种方式使用它。在第8章中,您看到了Environment Tag Helper,它用于根据当前环境显示和隐藏HTML。现在您知道它从哪里获得信息了——IHostEnvironment。

您可以使用类似的方法,通过在开发环境和生产环境中运行时加载不同的文件来定制在运行时加载的配置值。这是常见的,在大多数ASP.NET Core模板以及CreateDefaultBuilder帮助器方法中都是现成的。

11.5.2 加载特定于环境的配置文件

EnvironmentName值是在引导应用程序的过程早期确定的,在创建传递给ConfigureAppConfiguration的ConfigurationBuilder之前。这意味着您可以动态地更改将哪些配置提供程序添加到生成器中,从而在生成IConfiguration时加载哪些配置值。

一种常见的模式是具有一个可选的、特定于环境的appsettings.environment.json文件,该文件在默认appsettings.json文件之后加载。此列表显示了如果您在Program.cs中自定义ConfigureAppConfiguration方法,如何实现这一点。

清单11.14 添加特定于环境的appsettings.json文件

public class Program
{
    public static void AddAppConfiguration( HostBuilderContext hostingContext, IConfigurationBuilder config)
    {
        //当前IHostEnvironment在HostBuilderContext上可用。
        var env = hostingContext.HostingEnvironment;

        //添加一个可选的特定于环境的JSON文件,其中文件名随环境而异 
        config
            .AddJsonFile(
                "appsettings.json", optional: false)  //通常强制使用基本appsettings.json。
            .AddJsonFile
                $"appsettings.{env.EnvironmentName}.json",
                optional: true);
    }
}

使用此模式,全局appsettings.json文件包含适用于大多数环境的设置。其他可选的JSON文件appsettings.Development.JSON、appsettings.Sstaging.JSON和appsettings.Production.JSON随后会添加到ConfigurationBuilder中,具体取决于当前的EnvironmentName。

这些文件中的任何设置都将覆盖全局appsettings.json中的值,如果它们具有相同的键,如您之前所见。这允许您在开发环境中将日志设置为详细,并在生产环境中切换到更具选择性的日志。

另一种常见模式是根据环境完全添加或删除配置提供程序。例如,您可以在本地开发时使用User Secrets提供程序,但在生产中使用Azure Key Vault。此列表显示了如何使用IHostEnvironment仅在开发中有条件地包含User Secrets提供程序。

清单11.15 有条件地包含User Secrets配置提供程序

public class Program
{
    /* 附加程序配置 */ 
    public static void AddAppConfiguration(
    HostBuilderContext hostingContext, IConfigurationBuilder config)
    {
        var env = hostingContext.HostingEnvironment config
            .AddJsonFile(
                "appsettings.json", optional: false)
            .AddJsonFile(
                $"appsettings.{env.EnvironmentName}.json", optional: true);
 
        if(env.IsDevelopment())  //扩展方法使环境检查变得简单明了。
        {
            builder.AddUserSecrets<Startup>();  //在暂存和生产中,不会使用用户机密提供程序。
        }
    }
}

根据环境定制应用程序的中间件管道也是很常见的。在第3章中,您了解了DeveloperExceptionPageMiddleware以及在本地开发时应该如何使用它。下面的列表显示了如何使用IHostEnvironment以这种方式控制管道,以便在登台或生产时,应用程序使用ExceptionHandlerMiddleware。

清单11.16 使用托管环境定制中间件管道

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //在开发中,DeveloperExceptionPageMiddleware被添加到管道中。 
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else  //在暂存或生产中,管道使用ExceptionHandlerMiddleware。
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseHttpsRedirection(); 
    app.UseStaticFiles();

    app.UseRouting(); 
    app.UseAuthorization(); 
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

注意:在清单11.16中,我们注入了IWebHostEnvironment而不是IHostEnvironment。此接口通过添加WebRootPath属性(应用程序中wwwroot文件夹的路径)来扩展IHostEnvironment。我们不需要这条路,但意识到它的存在是很好的!

你可以在应用程序的任何地方注入IHostEnvironment,但我建议不要在启动和程序之外的自己的服务中使用它。最好使用配置提供程序根据当前宿主环境自定义强类型设置,并将这些设置注入到应用程序中。

如果您想在测试期间在不同的环境之间来回切换,那么使用环境变量设置IHostEnvironment可能会有点麻烦。就我个人而言,我总是忘记如何在我使用的各种操作系统上设置环境变量。我想教你的最后一个技巧是如何在本地开发时设置托管环境。

11.5.3 设置托管环境

在本节中,我将向您展示一些在开发时设置宿主环境的方法。这使得在不同环境中测试特定应用程序的行为变得容易,而无需更改机器上所有应用程序的环境。

如果ASP.NET Core应用程序启动时找不到SPNETCORE_ENVIRONMENT环境变量,则默认为生产环境,如图11.6所示。这意味着当您部署到生产环境时,默认情况下将使用正确的环境。

提示:默认情况下,当前宿主环境在启动时记录到控制台,这对于快速检查环境变量是否已正确拾取非常有用。

图11.6 默认情况下,ASP.NET Core应用程序在生产托管环境中运行。您可以通过设置ASP.NET Core_ENVIRONMENT变量来覆盖此设置。

另一个选项是使用launchSettings.json文件来控制环境。所有默认ASP.NET Core应用程序都在Properties文件夹中包含此文件。Launch

Settings.json定义用于运行应用程序的配置文件。

提示:配置文件可用于使用不同的环境变量运行应用程序。您还可以使用IIS Express配置文件,使用配置文件模拟在IIS后面的Windows上运行。就我个人而言,即使在Windows上,我也很少使用这个配置文件,我总是选择“项目”配置文件。

下面的列表中显示了一个典型的launchSettings.json文件,它定义了两个配置文件:“IIS Express”和“StoreViewerApplication”。后一个配置文件相当于使用dotnet run来运行项目,并且它的命名通常与包含launchSettings.json文件的项目相同。

清单11.17 定义两个配置文件的典型launchSettings.json文件

{
    //定义在IIS后面运行或使用IIS Express配置文件时的设置
    "iisSettings": 
        { "windowsAuthentication": false, 
         "anonymousAuthentication": true, 
         "iisExpress": {
            "applicationUrl": "http://localhost:53846", 
             "sslPort": 44399
        }
    },
    //默认情况下,在Windows上的Visual Studio中使用IIS Express配置文件。
    "profiles": {
        "IIS Express": { "commandName": "IISExpress", 
            //定义配置文件的自定义环境变量。
            "launchBrowser": true,   //如果为true,则在运行应用程序时启动浏览器
            "environmentVariables":   //将环境设置为“开发”。
            {
                "ASP.NET Core_ENVIRONMENT": "Development"
            }
    },
 
    //“项目”配置文件,相当于调用项目上的dotnet运行
    "StoreViewerApplication": { "commandName": "Project",   //如果为true,则在运行应用程序时启动浏览器
        "launchBrowser": true,
        //每个概要文件可以具有不同的环境变量。
        "environmentVariables": { "ASP.NET Core_ENVIRONMENT": "Development"},
        //定义应用程序在此配置文件中侦听的URL
        "applicationUrl":"https://localhost:5001;http://localhost:5000"
        }
    }	
}

在本地使用launchSettings.json文件的优点是,它允许您为项目设置“本地”环境变量。例如,在清单11.17中,环境被设置为开发环境。这允许您为每个项目甚至每个概要文件使用不同的环境变量,并将它们存储在源代码管理中。

您可以从工具栏上Debug按钮旁边的下拉列表中选择要在VisualStudio中使用的概要文件,如图11.7所示。您可以使用dotnet run --launch-profile<profile Name>从命令行选择要运行的配置文件。如果不指定配置文件,则使用第一个“项目”类型的配置文件。如果不想使用任何配置文件,则必须使用dotnet run --no-launch profile显式忽略launchSettings.json文件。

图11.7 通过从Debug下拉列表中选择,您可以从Visual Studio中选择要使用的概要文件。Visual Studio默认使用IIS Express配置文件。与dotnet运行一起运行的默认配置文件是第一个“项目”配置文件--在本例中是StoreViewerApplication。

如果您使用的是Visual Studio,还可以直观地编辑launchSettings.json文件:双击Properties节点并选择Debug选项卡。您可以在图11.8中看到ASP.NET Core_ENVIRONMENT设置为development;在此选项卡中所做的任何更改都会在launchSettings.json中进行镜像。

图11.8 如果愿意,可以使用Visual Studio编辑launchSettings.json文件。更改将在launchSettings.json文件和“属性”对话框之间进行镜像。

launchSettings.json文件仅用于本地开发;默认情况下,文件不会部署到生产服务器。虽然您可以在生产环境中部署和使用该文件,但通常不值得麻烦。环境变量更适合。

我在生产环境中设置环境的最后一个技巧是使用命令行参数。例如,可以将环境设置为分段,如下所示:

dotnet run --no-launch-profile --environment Staging

注意,如果有launchSettings.json文件,则还必须传递--no launch profile;否则文件中的值优先。

这就结束了关于配置的本章。配置并不迷人,但它是所有应用程序的重要组成部分。ASP.NET Core配置提供程序模型处理各种场景,允许您在各种位置存储设置和机密。

简单的设置可以存储在appsettings.json中,在开发过程中,它们很容易调整和修改,并且可以通过使用特定于环境的json文件来覆盖。同时,您的机密和敏感设置可以存储在用户机密管理器的项目文件之外,也可以作为环境变量。这为您提供了灵活性和安全性——只要您不将机密写入appsettings.json!

在下一章中,我们将简要介绍适合ASP.NET Core的新对象关系映射器:实体框架核心。我们在本书中只会尝试一下,但您将学习如何加载和保存数据,从代码中构建数据库,以及随着代码的发展迁移数据库。

总结

  • 任何可以被视为设置或秘密的内容通常都存储为配置值。
  • ASP.NET Core使用配置提供程序从各种源加载键值对。应用程序可以使用许多不同的配置提供程序。
  • ConfigurationBuilder描述了如何构建应用程序的最终配置表示,IConfiguration本身保存配置值。
  • 通过使用AddJsonFile()等扩展方法将配置提供程序添加到ConfigurationBuilder的实例,可以创建配置对象。HostBuilder为您创建ConfigurationBuilder实例,并调用Build()创建IConfiguration实例。
  • ASP.NET Core包括JSON文件、XML文件、环境文件和命令行参数等的内置提供程序。NuGet包适用于许多其他提供商,如YAML文件和Azure密钥库。
  • 向ConfigurationBuilder添加提供程序的顺序很重要;后续提供程序将替换先前提供程序中定义的设置值。
  • 配置键不区分大小写。
  • 您可以使用索引器语法直接从IConfiguration检索设置;例如,配置[“MySettings:Value”]。
  • CreateDefaultBuilder方法为您配置JSON、环境变量、命令行参数和User Secret提供程序。您可以通过调用ConfigureAppConfiguration自定义应用程序中使用的配置提供程序。
  • 在生产中,将机密存储在环境变量中。这些可以在配置生成器中基于文件的设置之后加载。
  • 在开发机器上,用户机密管理器是一个比使用环境变量更方便的工具。它将机密存储在项目文件夹之外的OS用户配置文件中。
  • 请注意,环境变量和用户机密管理器工具都不会加密机密,它们只会将机密存储在不太可能公开的位置,因为它们位于项目文件夹之外。
  • 基于文件的提供程序(如JSON提供程序)可以在文件更改时自动重新加载配置值。这允许您实时更新配置值,而无需重新启动应用程序。
  • 使用强类型POCO选项类访问应用程序中的配置。
  • 使用ConfigureServices中的Configure<T>()扩展方法将POCO选项对象绑定到ConfigurationSection。
  • 您可以使用DI将IOptions<T>接口注入到服务中。您可以访问Value属性上的强类型选项对象。
  • 通过向configure()方法传递lambda,您可以在代码中配置IOptions<T>对象,而不是使用配置值。
  • 如果要在配置更改时重新加载POCO选项对象,请改用IOptionsSnapshot<T>界面。
  • 在不同环境中运行的应用程序(例如开发与生产)通常需要不同的配置值。
  • ASP.NET Core使用ASP.NET Core_environment环境变量确定当前宿主环境。如果未设置此变量,则假定环境为生产环境。
  • 您可以使用launchSettings.json文件在本地设置宿主环境。这允许您将环境变量的范围限定到特定项目。
  • 当前宿主环境公开为IHostEnvironment接口。可以使用IsDevelopment()、IsStaging()和IsProduction()检查特定环境。
  • 您可以使用IHostEnvironment对象加载特定于当前环境的文件,例如appsettings.Production.json。

 

posted on 2023-01-31 15:56  生活的倒影  阅读(135)  评论(0编辑  收藏  举报