翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-5.0
ASP.NET Core 应用程序配置和启动一个 Host。Host 负责应用程序的启动和生命周期的管理。至少的,Host 配置一个服务器和一个请求处理管道。Host 也会设置日志,依赖注入和配置。
这篇文章覆盖了 Web Host,任然是只是向后兼容可用。 Generic Host 推荐用于所有类型的应用程序。
配置一个 Host
使用 IWebHostBuilder 的实例创建一个 Host。一般会在应用程序的入口点 Main 方法中创建。
在工程模板中,Main 方法位于 Program.cs 中。典型的应用程序调用 CreateDefaultBuilder 启动配置一个 Host:
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
代码中调用 CreateDefaultBuilder 的是在一个叫做 CreateWebHostBuilder 的方法中,这个方法从 Main 中分离出来,在 Main 方法中在 builder 对象上调用 Run 方法。这中分离是有必须的,如果你使用了 Entity Framework Core tools。这个工具期望找到一个 CreateWebHostBuilder 方法,以便能不必运行应用程序就能够在设计的时候配置 Host。一种途径是实现接口 IDesignTimeDbContextFactory。更多信息,查看 Design-time DbContext Creation。
CreateDefaultBuilder 执行了以下任务:
- 使用应用程序的托管配置的应用程序提供器配置 Kestrel 服务器作为 web 服务器。Kestrel 服务器默认选项,查看 Configure options for the ASP.NET Core Kestrel web server。
- 设置 content root 为 Directory.GetCurrentDirectory 返回的路径值
- 从以下地方加载主机配置(host configuration)
前缀为 ASPNETCORE_ 的环境变量(例如,ASPNETCORE_ENVIRONMENT)
命令行参数 - 按照下面的顺序加载应用程序配置
appsetting.json
appsetting.{Environment}.json
User secrets 当应用程序使用入口程序集运行在 Development 环境时
环境变量
命令行参数 - 为控制台和调试输出配置日志(logging)。日志包括在 appsetting.json 或者 appsetting.{Environment}.json 文件中配置区域配置的日志过滤(log filtering)规则。
- 当 ASP.NET Core Module 运行在 IIS 之后,CreateDefaultBuilder 会使能 IIS 集成(IIS Integration),配置了应用程序基本地址和端口。IIS 集成也会配置应用程序捕获启动错误(capture startup errors)。关于 IIS 默认配置,查看 Host ASP.NET Core on Windows with IIS。
- 如果应用程序环境为 Development,设置 ServiceProviderOptions.ValidateScopes 为 true。更多信息,查看 Scope validation。
CreateDefaultBuilder 定义的配置可以被覆盖和使用 ConfigureAppConfiguration,ConfigureLogging 和其它方法及 IWebHostBuilder 的扩展方法扩展。下面是几个例子:
- ConfigureAppConfiguration 用来为应用程序指定额外的 IConfiguration。下面的 ConfigureAppConfiguration 调用添加了一个代理去包含在 appsettings.xml 文件中的应用程序的配置。ConfigureAppConfiguration 可能会调用多次。注意这个配置并不应用到主机(例如,服务器 URLs 或者 环境)。查看 Host configuration values 部分。
WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true); }) ...
- 下面的 ConfigureLogging 调用添加一个代理去配置最小的logging level (SetMinimumLevel) 为 LogLevel.Warning。这个设置覆盖了appsettings.Development.json (LogLevel.Debug) 和 appsettings.Production.json (LogLevel.Error) 里面通过 CreateDefaultBuilder 配置的设置。ConfigureLogging 可能会调用多次。
WebHost.CreateDefaultBuilder(args) .ConfigureLogging(logging => { logging.SetMinimumLevel(LogLevel.Warning); }) ...
- 下面的 ConfigureKestrel 的调用覆盖了 Kestrel 通过 CreateDefaultBuilder 配置的默认的 Limits.MaxRequestBodySize 30,000,000 字节:
WebHost.CreateDefaultBuilder(args) .ConfigureKestrel((context, options) => { options.Limits.MaxRequestBodySize = 20000000; });
content root 决定了 Host 在哪里搜索内容文件,例如 MVC 视图文件。当应用程序从工程根目录启动的时候,工程根目录被用作内容根目录。这在 Visual Studio 和 dotnet new templates 默认使用。
更多关于应用程序配置的信息,查看 Configuration in ASP.NET Core。
注意
作为一种使用静态 CreateDefaultBuilder 方法的途径,从 WebHostBuilder 创建一个 Host 在 ASP.NET Core 2.x 中是受支持的。
当设置一个主机的时候,可以提供 Configure 和 ConfigureServices 这两个方法。如果一个 Startup 方法被指定了,它必须定义 Configure 方法。更多信息,查看 App startup in ASP.NET Core 。多次调用 ConfigureServices 将会附加到另外一个上面。在 WebHostBuilder 上多次调用 Configure 或者 UseStartup 将会替换之前的设置。
Host configuration values
WebHostBuilder 依赖下面的方法设置主机配置值:
- Host builder configuration,包含格式为 ASPNETCORE_{configurationKey} 的环境变量。例如,ASPNETCORE_ENVIRONMENT。
- 扩展,例如 UseContentRoot 和 UseConfiguration (查看 Override configuration 部分)
- UseSetting 和相关的键。当使用 UseSetting 设置值得时候,值被设置为字符串而忽略它的类型。
主机使用最后设置值得选项。更多信息查看,Override configuration。
Application Key (Name)
当 UseStartup 或者 Configure 在主机构造方法中调用的时候,IWebHostEnvironment.ApplicationName 属性会自动设置。值被设置为包含应用程序入口点的程序集的名称。显式的设置,可以使用 WebHostDefaults.ApplicationKey:
Key: applicationName
Type: string
Default: 包含应用程序入口点程序集的名称
Set using: UseSetting
Environment variable: ASPNETCORE_APPLICATIONNAME
WebHost.CreateDefaultBuilder(args) .UseSetting(WebHostDefaults.ApplicationKey, "CustomApplicationName")
Capture Startup Errors
设置捕获启动错误的控制
Key: captureStartupErrors
Type: bool (true or 1)
Default: 默认为 false,除非应用程序使用 Kestrel 运行在 IIS 之后,这时默认是 true
Set using: CaptureStartupErrors
Environment variable: ASPNETCORE_CAPTURESTARTUPERRORS
当设置为 false 时,启动过程中的错误会导致主机退出。当设置为 true 时,主机会捕获启动过程中的异常,并且试图启动服务器。
WebHost.CreateDefaultBuilder(args) .CaptureStartupErrors(true)
Content root
这个设置决定了 ASP.NET Core 开始搜索内容文件的位置。
Key: contentRoot
Type: string
Default: 默认是应用程序程序集所在的目录
Set using: UseContentRoot
Environment variable: ASPNETCORE_CONTENTROOT
content root 也被用作 web root 的基本路径。如果 content root 路径不存在,主机就会启动失败。
WebHost.CreateDefaultBuilder(args) .UseContentRoot("c:\\<content-root>")
更多信息,请查看:
- Fundamentals: Content root
- Web root
Detailed Errors
决定是否应该详细错误信息
Key: detailedErrors
Type: bool (treu 或者 1)
Default: false
Set using: UseSetting
Environment variable: ASPNETCORE_DETAILEDERRORS
当使能的时候(或者 Environment 被设置为 Development 的时候),应用程序会捕获异常详细信息。
WebHost.CreateDefaultBuilder(args) .UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
Environment
设置应用程序环境
Key: environment
Type: string
Default: Production
Set using: UseEnvironment
Environment variable: ASPNETCORE_ENVIRONMENT
environmenmt 可以被设置为任意的值。框架定义的值包括 Development,Staging 和 Production。值不区分大小写。默认的,Environment 从 ASPNETCORE_ENVIRONMENT 环境变量中读取。当使用 Visual Studio 时,环境变量可能在 lauchSetting.json 文件中设置。更过信息,请查看: Use multiple environments in ASP.NET Core。
WebHost.CreateDefaultBuilder(args)
.UseEnvironment(EnvironmentName.Development)
Hosting Startup Assemblies
设置应用程序托管启动程序集
Key: hostingStartupAssemblies
Type: string
Default: Empty string
Set using: UseSetting
Environment variable: ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
以逗号分隔的字符串,启动时加载的托管的启动程序集
尽管配置值被设置为空字符串,托管程序集总是包含应用程序程序集。当提供了托管启动程序集,它们在应用程序启动时创建公共服务时被添加到应用程序程序集。
WebHost.CreateDefaultBuilder(args) .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2")
HTTPs Port
设置 HTTPS 重定向端口。使用 enforcing HTTPS。
Key: https_port
Type: string
Defalut: 默认无设置
Set using: UseSetting
Environment variable: ASPNETCORE_HTTPS_PORT
WebHost.CreateDefaultBuilder(args) .UseSetting("https_port", "8080")
Hosting Startup Exclude Assemblies
冒号分隔的字符串,启动时排除托管启动程序集
Key: hostingStartupExcludeAssemblies
Type: string
Default: Empty string
Set using: UseSetting
Environment variable: ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES
WebHost.CreateDefaultBuilder(args) .UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, "assembly1;assembly2")
Prefer Hosting URLs
表明主机是否应该在使用 WebHostBuilder 配置的 URLs 上监听,而不是 IServer 实现配置的 URLs
Key: preferHostingUrls
Type: bool (true 或者 1)
Default: true
Set using: PreferHostingUrls
Environment variable: ASPNETCORE_PREFERHOSTINGURLS
WebHost.CreateDefaultBuilder(args) .PreferHostingUrls(false)
Prevent Hosting Startup
阻止自动加载托管启动程序集,包括应用程序程序集配置的托管启动程序集。更多信息查看,Use hosting startup assemblies in ASP.NET Core。
Key: preventHostingStartup
Type: bool (true 或者 1)
Default: false
Set using: UseSetting
Environment variable: ASPNETCORE_PREVENTHOSTINGSTARTUP
WebHost.CreateDefaultBuilder(args) .UseSetting(WebHostDefaults.PreventHostingStartupKey, "true")
Server URLs
表明带有端口和协议的 IP 地址或者主机地址是否应该被服务器监听请求
Key: urls
Type: string
Default: http://localhost:5000
Set using: UseUrls
Environment variable: ASPNETCORE_URLS
设置一组服务器应该响应的冒号(;)分隔的 URL 前缀。例如,http://localhost:123。使用 “*” 表明服务器是否应该监听任意使用特定端口和协议(例如,http://*:5000)的 IP 地址或者主机地址。协议 (http:// 或者 https://) 必须包含在每一个 URL 中。支持的格式因服务器不同而不同。
WebHost.CreateDefaultBuilder(args) .UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")
Kestrel 有它自己的 endpoint 配置 API。更多信息查看,Configure endpoints for the ASP.NET Core Kestrel web server。
Shutdown Timeout
指定等待 Web Host 关闭的超时时间
Key: shutdownTimeoutSeconds
Type: int
Default: 5
Set using: UseShutdownTimeout
Environment variable: ASPNETCORE_SHUTDOWNTIMEOUTSECONDS
尽管使用 UseSetting 可以接受键为 int 的值(例如,.UseSetting(WebHostDefaults.ShutdownTimeoutKey,”10″)),UseShutdownTimeout 带有 TimeSpan 参数。
在超时时间内,主机会:
- 触发 IApplicationLifetime.ApplicationStopping
- 尝试停止托管的服务,日志记录任何服务停止失败的错误
如果在所有服务停止之前超时了,任何活动的服务在应用程序关闭时都会停止。即使服务没有完成处理也会被停止。如果服务需要更多的时间去停止,增加超时时间。
WebHost.CreateDefaultBuilder(args) .UseShutdownTimeout(TimeSpan.FromSeconds(10))
Startup Assembly
决定搜索 Startup 类的程序集
Key: startupAssembly
Type: string
Default: 应用程序程序集
Set using: UseStartup
Environment variable: ASPNETCORE_STARTUPASSEMBLY
可以指定程序的名称(string)或者类型 (TStartup)。如果多个 UseStartup 方法被调用,则最后一个优先级最高:
WebHost.CreateDefaultBuilder(args) .UseStartup("StartupAssemblyName")
WebHost.CreateDefaultBuilder(args)
.UseStartup<TStartup>()
Web root
设置应用程序静态资源的相对路径
Key: webroot
Type: string
Default: 默认是 wwwroot。路基 {contentroot}/wwwroot 必须存在。如果路径不存在,一个 no-op 文件提供器将被使用。
WebHost.CreateDefaultBuilder(args) .UseWebRoot("public")
更多信息查看:
- Fundamentals: Web root
- Content root
覆盖配置
使用 Configuration 配置 Web Host。在下面的例子中,host 配置在 hostsetting.json 文件中是可选指定的。任何从 hostsetting.json 文件中加载的配置可能会被命令行参数覆盖。编译的配置(in config)使用 UseConfiguration 来配置主机。IWebHostBuilder 配置被添加到应用程序配置中,但是相反的就不是,ConfigureAppConfigureation 不会影响 IWebHostBuilder 配置。
覆盖 UseUrls 提供的配置优先使用 hostsettings.json 配置,其次是命令行参数配置:
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) { var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("hostsettings.json", optional: true) .AddCommandLine(args) .Build(); return WebHost.CreateDefaultBuilder(args) .UseUrls("http://*:5000") .UseConfiguration(config) .Configure(app => { app.Run(context => context.Response.WriteAsync("Hello, World!")); }); } }
hostsettings.json:
{ urls: "http://*:5005" }
注意
UseConfiguration 只会复制 IConfiguration 提供的键值到 host builder 配置。因此,设置 reloadOnChange: true 为 JSON,INI,和 XML 设置文件没有影响。
指定主机在一个特定的 URL 上运行,期望的值可以在运行 dotnet run 时命令行提示中传入。命令行参数覆盖了来自 hostsettings.json 中的 urls 值,服务器在 8080 端口监听:
dotnet run --urls "http://*:8080"
管理 Host
Run
Run 方法启动 web 应用程序并阻塞调用线程直到 Host 关闭:
host.Run();
Start
通过调用它的 Start 方法以 non-blocking 方式运行 Host:
using (host) { host.Start(); Console.ReadLine(); }
如果一组 URLs 传递给 Start 方法,它就会监听这组指定的 URLs:
var urls = new List<string>() { "http://*:5000", "http://localhost:5001" }; var host = new WebHostBuilder() .UseKestrel() .UseStartup<Startup>() .Start(urls.ToArray()); using (host) { Console.ReadLine(); }
应用程序可以使用预设值的默认 CreateDefaultBuilder 使用一个静态约定的方法初始化和启动一个新的 Host。这些方法启动服务器时没有控制台输出,使用 WaitForShutdown 等待终止(Ctrl-C/SIGINT 或者 SIGTERM):
Start(RequestDelegate app)
使用 RequestDelegate 启动:
using (var host = WebHost.Start(app => app.Response.WriteAsync("Hello, World!"))) { Console.WriteLine("Use Ctrl-C to shutdown the host..."); host.WaitForShutdown(); }
在浏览器中发送一个请求 http://localhost:5000 接收到 “Hello World!” 响应,WaitForShutdown 阻塞了直到一个结束信号 (Ctrl-C/SIGINT 或者 SIGTERM) 出现。应用程序显示了 Console.WriteLine 信息,等待按键退出。
Start(string url, RequestDelegate app)
使用一个 URL 和 RequestDelegate 启动应用程序:
using (var host = WebHost.Start("http://localhost:8080", app => app.Response.WriteAsync("Hello, World!"))) { Console.WriteLine("Use Ctrl-C to shutdown the host..."); host.WaitForShutdown(); }
和 Start(RequestDelegate app) 生成同样的结果,期望应用程序在 http://localhost:8080 上响应。
Start(Action <IRouteBuilder> routerBuilder)
使用 IRouteBuilder (Microsoft.AspNetCore.Routing) 的实例使用 routing 中间件:
using (var host = WebHost.Start(router => router .MapGet("hello/{name}", (req, res, data) => res.WriteAsync($"Hello, {data.Values["name"]}!")) .MapGet("buenosdias/{name}", (req, res, data) => res.WriteAsync($"Buenos dias, {data.Values["name"]}!")) .MapGet("throw/{message?}", (req, res, data) => throw new Exception((string)data.Values["message"] ?? "Uh oh!")) .MapGet("{greeting}/{name}", (req, res, data) => res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!")) .MapGet("", (req, res, data) => res.WriteAsync("Hello, World!")))) { Console.WriteLine("Use Ctrl-C to shutdown the host..."); host.WaitForShutdown(); }
对上面的示例使用下面的浏览器请求:
Request | Response |
http://localhost:5000/hello/Martin | Hello,Martin! |
http://localhost:5000/buenosdias/Catrina | Buenos dias,Catrina! |
http://localhost:5000/throw/ooops! | Throw an exception with string “ooops!” |
http://localhost:5000/throw | Throw an exception with string “Uh oh!” |
http://localhost:5000/Sante/Kevin | Sante,Kevin! |
http://localhost:5000 | Hello World! |
WaitForShutdown 阻塞直到结束信号(Ctrl-C/SIGINT 或者 SIGTERM)出现。应用程序显示 Console.WriteLine 信息,等待按键按下退出。
Start(string url, Action<IRouteBuilder> routeBuilder)
使用 URL 和 IRouterBuilder 实例:
using (var host = WebHost.Start("http://localhost:8080", router => router .MapGet("hello/{name}", (req, res, data) => res.WriteAsync($"Hello, {data.Values["name"]}!")) .MapGet("buenosdias/{name}", (req, res, data) => res.WriteAsync($"Buenos dias, {data.Values["name"]}!")) .MapGet("throw/{message?}", (req, res, data) => throw new Exception((string)data.Values["message"] ?? "Uh oh!")) .MapGet("{greeting}/{name}", (req, res, data) => res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!")) .MapGet("", (req, res, data) => res.WriteAsync("Hello, World!")))) { Console.WriteLine("Use Ctrl-C to shut down the host..."); host.WaitForShutdown(); }
生成和 Start(Action<IRouteBuilder> routeBuilder) 相同的结果,期望应用程序在 http://localhost:8080 上响应。
StartWith(Action<IApplicationBuilder> app)
提供一个代理配置一个 IApplicationBuilder:
using (var host = WebHost.StartWith(app => app.Use(next => { return async context => { await context.Response.WriteAsync("Hello World!"); }; }))) { Console.WriteLine("Use Ctrl-C to shut down the host..."); host.WaitForShutdown(); }
在浏览器中请求 http://localhost:5000 接收到 “Hello World!” 响应,WaitForShutdown 阻塞直到一个结束信号(Ctrl-C/SIGINT 或者 SIGTERM)发出。应用程序显示了 Console.WriteLine 信息,等待按键按下退出。
StartWith(string url, Action<IApplicationBuilder> app)
提供一个 url 和一个代理配置 IApplicationBuilder:
using (var host = WebHost.StartWith("http://localhost:8080", app => app.Use(next => { return async context => { await context.Response.WriteAsync("Hello World!"); }; }))) { Console.WriteLine("Use Ctrl-C to shut down the host..."); host.WaitForShutdown(); }
结果和 StartWith(Action<IApplicatonBuilder> app) 相同的结果,期望在应用程序在 http://localhost:8080 上响应。
IWebHostEnvironment interface
IWebHostEnvironment 接口提供了关于应用程序 web 托管环境的信息。使用 constructor injection 访问 IWebHostEnvironment 保证使用它的属性和扩展方法:
public class CustomFileReader { private readonly IWebHostEnvironment _env; public CustomFileReader(IWebHostEnvironment env) { _env = env; } public string ReadFile(string filePath) { var fileProvider = _env.WebRootFileProvider; // Process the file here } }
一个 convention-based approach 可以基于环境在启动时用来配置应用程序。或者,在 ConfigureServices 中将 IWebHostEnvironment 注入到 Startup 构造方法使用:
public class Startup { public Startup(IWebHostEnvironment env) { HostingEnvironment = env; } public IWebHostEnvironment HostingEnvironment { get; } public void ConfigureServices(IServiceCollection services) { if (HostingEnvironment.IsDevelopment()) { // Development configuration } else { // Staging/Production configuration } var contentRootPath = HostingEnvironment.ContentRootPath; } }
注意
除了 IsDevelopment 扩展方法外,IWebHostEnvironment 提供了 IsStaging,IsProduction 和 IsEnvironment(string environment) 方法。更多信息查看,Use multiple environments in ASP.NET Core。
IWebHostEnvironment 服务也可以直接注入到 Configure 方法用来设置处理管道:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // In Development, use the Developer Exception Page app.UseDeveloperExceptionPage(); } else { // In Staging/Production, route exceptions to /error app.UseExceptionHandler("/error"); } var contentRootPath = env.ContentRootPath; }
当创建自定义中间件(middleware)的时候,IWebHostEnvironment 可以被注入到 Invoke 方法:
IHostApplicationLifetime interface
IHostApplicationLifetime 允许 post-startup 和 关闭。接口的三个属性是取消令牌,用来注册定义启动和关闭时间的Action 方法。
Cancellation Token | Triggered when… |
ApplicationStarted | 主机已经完全启动 |
ApplicationStopped | 主机已经完全正常关闭。所有的请求应该已经处理完毕。关闭阻塞直到这个事件完成。 |
ApplicationStopping | 主机正在正常关闭。请求可能仍然正在处理。关闭阻塞直到这个事件完成。 |
public class Startup { public void Configure(IApplicationBuilder app, IHostApplicationLifetime appLifetime) { appLifetime.ApplicationStarted.Register(OnStarted); appLifetime.ApplicationStopping.Register(OnStopping); appLifetime.ApplicationStopped.Register(OnStopped); Console.CancelKeyPress += (sender, eventArgs) => { appLifetime.StopApplication(); // Don't terminate the process immediately, wait for the Main thread to exit gracefully. eventArgs.Cancel = true; }; } private void OnStarted() { // Perform post-startup activities here } private void OnStopping() { // Perform on-stopping activities here } private void OnStopped() { // Perform post-stopped activities here } }
StopApplication 请求结束应用程序。下面的类使用 StopApplication 正常关闭应用程序当类的 Shutdown 方法被调用的时候:
public class MyClass { private readonly IHostApplicationLifetime _appLifetime; public MyClass(IHostApplicationLifetime appLifetime) { _appLifetime = appLifetime; } public void Shutdown() { _appLifetime.StopApplication(); } }
Scope validation
当应用程序的环境是 Development 的时候,CreateDefaultBuilder 设置 ServiceProviderOptions.ValidateScopes 为 true。
当 ValidateScopes 被设置为 true 时,默认的服务提供器执行检查验证:
- Scoped 服务不能直接或者间接的从根服务提供器中解析出来
- Scoped 服务不能直接或者间接的注入到单例中
根服务提供器在 BuildServiceProvider 被调用时创建。根服务提供器的生命周期和应用程序/服务器的生命周期一致,当提供器和应用程序一起启动,在应用程序关闭时释放。
Scoped 服务由创建它的容器释放。如果一个 scoped 服务在根容器中创建,服务的生命周期会有效的提升为单例,因为它只有在应用程/服务器关闭的时候会释放掉。在调用 BuildServiceProvider 的时候,验证服务会捕捉这些情况。
为了在 Production 环境中总是包含 validate scopes,在 Host builer 上使用 UseDefaultServiceProvider 配置 ServiceProviderOptions:
WebHost.CreateDefaultBuilder(args) .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = true; })