本篇是如何升级到 ASP.NET Core 3.0 系列文章的第二篇.
Part 1 - 将. NET Standard 2.0 类库转换为. NET Core 3.0 类库
- Part 2 -
- IHostingEnvironment
VS IHostEnvironent - .NET Core 3.0 中的废弃类型
Part 3 - 避免在 ASP.NET Core 3.0 中为启动类注入服务(本篇)
Part 4 - 将终端中间件转换为 ASP.NET Core 3.0 中的端点路由
Part 5 - 将集成测试的转换为 NET Core 3.0
在本篇博客中, 我将描述从 ASP.NET Core 2.x 应用升级到. NET Core 3.0 需要做的一个修改: 你不在需要在 Startup 构造函数中注入服务了.
在 ASP.NET Core 3.0 中迁移到通用主机
在. NET Core 3.0 中, ASP.NET Core 3.0 的托管基础已经被重新设计为通用主机, 而不再与之并行使用. 那么这对于那些正在使用 ASP.NET Core 2.x 开发应用的开发人员, 这意味着什么呢? 在目前这个阶段, 我已经迁移了多个应用, 到目前为止, 一切都进展顺利. 官方的迁移指导文档可以很好的指导你完成所需的步骤, 因此, 我强烈建议你读一下这篇文档.
在迁移过程中, 我遇到的最多两个问题是:
ASP.NET Core 3.0 中配置中间件的推荐方式是使用端点路由(Endpoint Routing).
通用主机不允许为 Startup 类注入服务
其中第一点, 我之前已经讲解过了. 端点路由 (Endpoint Routing) 是在 ASP.NET Core 2.2 中引入的, 但是被限制只能在 MVC 中使用. 在 ASP.NET Core 3.0 中, 端点路由已经是推荐的终端中间件实现了, 因为它提供了很多好处. 其中最重要的是, 它允许中间件获取哪一个端点最终会被执行, 并且可以检索有关这个端点的元数据(metadata). 例如, 你可以为健康检查端点应用授权.
端点路由是在配置中间件顺序时需要特别注意. 我建议你再升级你的应用前, 先阅读一下官方迁移文档针对此处的说明, 后续我将写一篇博客来介绍如何将终端中间件转换为端点路由.
第二点, 是已经提到了的将服务注入 Startup 类, 但是并没有得到足够的宣传. 我不太确定是不是因为这样做的人不多, 还是在一些场景下, 它很容易解决. 在本篇中, 我将展示一些问题场景, 并提供一些解决方案.
ASP.NET Core 2.x 启动类中注入服务
在 ASP.NET Core 2.x 版本中, 有一个鲜为人知的特性, 就是你可以在 Program.cs 文件中配置你的依赖注入容器. 以前我曾经使用这种方式来进行强类型选项, 然后在配置依赖注入容器的其余剩余部分时使用这些配置.
下面我们来看一下 ASP.NET Core 2.x 的例子:
- public class Program
- {
- public static void Main(string[] args)
- {
- CreatewebHostBuilder(args).Build().Run();
- }
- public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
- WebHost.CreateDefaultBuilder(args)
- .UseStartup<Startup>()
- .ConfigureSettings(); // 配置服务, 后续将在 Startup 中使用
- }
这里有没有注意到在 CreateWebHostBuilder 中调用了一个 ConfigureSettings()的方法? 这是一个我用来配置应用强类型选项的扩展方法. 例如, 这个扩展方法可能看起来是这样的:
- public static class SettingsinstallerExtensions
- {
- public static IWebHostBuilder ConfigureSettings(this IWebHostBuilder builder)
- {
- return builder.ConfigureServices((context, services) =>
- {
- var config = context.Configuration;
- services.Configure<ConnectionStrings>(config.GetSection("ConnectionStrings"));
- services.AddSingleton<ConnectionStrings>(
- ctx => ctx.GetService<IOptions<ConnectionStrings>>().Value)
- });
- }
- }
所以这里, ConfigureSettings()方法调用了 IWebHostBuilder 实例的 ConfigureServices()方法, 配置了一些设置. 由于这些服务会在 Startup 初始化之前被配置到依赖注入容器, 所以在 Startup 类的构造函数中, 这些以配置的服务是可以被注入的.
- public static class Startup
- {
- public class Startup
- {
- public Startup(
- IConfiguration configuration,
- ConnectionStrings ConnectionStrings) // 注入预配置服务
- {
- Configuration = configuration;
- ConnectionStrings = ConnectionStrings;
- }
- public IConfiguration Configuration { get; }
- public ConnectionStrings ConnectionStrings { get; }
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- // 使用配置中的连接字符串
- services.AddDbContext<BloggingContext>(options =>
- options.UseSqlServer(ConnectionStrings.BloggingDatabase));
- }
- public void Configure(IApplicationBuilder App)
- {
- }
- }
- }
我发现, 当我先要在 ConfigureServices 方法中使用强类型选项对象配置其他服务时, 这种模式非常的有用. 在我上面的例子中, ConnectionStrings 对象是一个强类型对象, 并且这个对象在程序进入 Startup 之前, 就已经进行非空验证. 这并不是一种正规的基础技术, 但是实时证明使用起来非常的顺手.
PS: 如何为 ASP.NET Core 的强类型选项对象添加验证
然而, 如果切换到 ASP.NET Core 3.0 通用主机之后, 你会发现这种实现方式在运行时会收到以下的错误信息.
- Unhandled exception. System.InvalidOperationException: Unable to resolve service for type 'ExampleProject.ConnectionStrings' while attempting to activate 'ExampleProject.Startup'.
- at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
- at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
- at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
- at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass12_0.<UseStartup>b__0(HostBuilderContext context, IServiceCollection services)
- at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
- at Microsoft.Extensions.Hosting.HostBuilder.Build()
- at ExampleProject.Program.Main(String[] args) in C:\repos\ExampleProject\Program.cs:line 21
这种方式在 ASP.NET Core 3.0 中已经不再支持了. 你可以在 Startup 类的构造函数注入 IHostEnvironment 和 IConfiguration, 但是仅此而已. 至于原因, 应该是之前的实现方式会带来一些问题, 下面我将给大家详细描述一下.
注意: 如果你坚持在 ASP.NET Core 3.0 中使用 IWebHostBuilder, 而不使用的通用主机的话, 你依然可以使用之前的实现方式. 但是我强烈建议你不要这样做, 并尽可能的尝试迁移到通用主机的方式.
两个单例?
注入服务到 Startup 类的根本问题是, 它会导致系统需要构建依赖注入容器两次. 在我之前展示的例子中, ASP.NET Core 知道你需要一个 ConnectionStrings 对象, 但是唯一知道如何构建该对象的方法是基于 "部分" 配置构建 IServiceProvider(在之前的例子中, 我们使用 ConfigureSettings()扩展方法提供了这个 "部分" 配置).
那么为什么这个会是一个问题呢? 问题是这个 ServiceProvider 是一个临时的 "根"ServiceProvider. 它创建了服务并将服务注入到 Startup 中. 然后, 剩余的依赖注入容器配置将作为 ConfigureServices 方法的一部分运行, 并且临时的 ServiceProvider 在这时就已经被丢弃了. 然后一个新的 ServiceProvider 会被创建出来, 在其中包含了应用程序 "完整" 的配置.
这样, 即使服务配置使用 Singleton 生命周期, 也会被创建两次:
当使用 "部分"ServiceProvider 时, 创建了一次, 并针对 Startup 进行了注入
当使用 "完整"ServiceProvider 时, 创建了一次
对于我的用例, 强类型选项, 这可能是无关紧要的. 系统并不是只可以有一个配置实例, 这只是一个更好的选择. 但是这并非总是如此. 服务的这种 "泄露" 似乎是更改通用主机行为的主要原因 - 它让东西看起来更安全了.
那么如果我需要 ConfigureServices 内部的服务怎么办?
虽然我们已经不能像以前那样配置服务了, 但是还是需要一种可以替换的方式来满足一些场景的需要!
其中最常见的场景是通过注入服务到 Startup, 针对 Startup.ConfigureServices 方法中注册的其他服务进行状态控制. 例如, 以下是一个非常基本的例子.
- public class Startup
- {
- public Startup(IdentitySettings identitySettings)
- {
- IdentitySettings = identitySettings;
- }
- public IdentitySettings IdentitySettings { get; }
- public void ConfigureServices(IServiceCollection services)
- {
- if(IdentitySettings.UseFakeIdentity)
- {
- services.AddScoped<IIdentityService, FakeIdentityService>();
- }
- else
- {
- services.AddScoped<IIdentityService, RealIdentityService>();
- }
- }
- public void Configure(IApplicationBuilder App)
- {
- // ...
- }
- }
这个例子中, 代码通过检查注入的 IdentitySettings 对象中的布尔值属性, 决定了 IIdentityService 接口使用哪个实现来注册: 或者使用假服务, 或者使用真服务.
通过将静态服务注册转换为工厂函数的方式, 可以使需要注入 IdentitySetting 对象的实现方式与通用主机兼容. 例如:
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
- public IConfiguration Configuration { get; }
- public void ConfigureServices(IServiceCollection services)
- {
- // 为依赖注入容器, 配置 IdentitySetting
- services.Configure<IdentitySettings>(Configuration.GetSection("Identity"));
- // 注册不同的实现
- services.AddScoped<FakeIdentityService>();
- services.AddScoped<RealIdentityService>();
- // 根据 IdentitySetting 配置, 在运行时返回一个正确的实现
- services.AddScoped<IIdentityService>(ctx =>
- {
- var identitySettings = ctx.GetRequiredService<IdentitySettings>();
- return identitySettings.UseFakeIdentity
- ? ctx.GetRequiredService<FakeIdentityService>()
- : ctx.GetRequiredService<RealIdentityService>();
- }
- });
- }
- public void Configure(IApplicationBuilder App)
- {
- // ...
- }
- }
这个实现显然比之前的版本要复杂的多, 但是至少可以兼容通用主机的方式.
实际上, 如果仅需要一个强类型选项, 那么这个方法就有点过头了. 相反的, 这里我可能只会重新绑定一下配置:
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
- public IConfiguration Configuration { get; }
- public void ConfigureServices(IServiceCollection services)
- {
- // 为依赖注入容器, 配置 IdentitySetting
- services.Configure<IdentitySettings>(Configuration.GetSection("Identity"));
- // 重新创建强类型选项对象, 并绑定
- var identitySettings = new IdentitySettings();
- Configuration.GetSection("Identity").Bind(identitySettings)
- // 根据条件配置正确的服务
- if(identitySettings.UseFakeIdentity)
- {
- services.AddScoped<IIdentityService, FakeIdentityService>();
- }
- else
- {
- services.AddScoped<IIdentityService, RealIdentityService>();
- }
- }
- public void Configure(IApplicationBuilder App)
- {
- // ...
- }
- }
除此之外, 如果仅仅只需要从配置文件中加载一个字符串, 我可能根本不会使用强类型选项. 这是. NET Core 默认模板中拥堵配置 ASP.NET Core 身份系统的方法 - 直接通过 IConfiguration 实例检索连接字符串.
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
- public IConfiguration Configuration { get; }
- public void ConfigureServices(IServiceCollection services)
- {
- // 针对依赖注入容器, 配置 ConnectionStrings
- services.Configure<ConnectionStrings>(Configuration.GetSection("ConnectionStrings"));
- // 直接获取配置, 不使用强类型选项
- var connectionString = Configuration["ConnectionString:BloggingDatabase"];
- services.AddDbContext<ApplicationDbContext>(options =>
- options.UseSqlite(connectionString));
- }
- public void Configure(IApplicationBuilder App)
- {
- // ...
- }
- }
这个实现方式都不是最好的, 但是他们都可以满足我们的需求, 以及大部分的场景. 如果你以前不知道 Startup 的服务注入特性, 那么你肯定使用了以上方式中的一种.
使用 IConfigureOptions 来对 IdentityServer 进行配置
另外一个使用注入配置的常见场景是配置 IdentityServer 的验证.
- public class Startup
- {
- public Startup(IdentitySettings identitySettings)
- {
- IdentitySettings = identitySettings;
- }
- public IdentitySettings IdentitySettings { get; }
- public void ConfigureServices(IServiceCollection services)
- {
- // 配置 IdentityServer 的验证方式
- services
- .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
- .AddIdentityServerAuthentication(options =>
- {
- // 使用强类型选项来配置验证处理器
- options.Authority = identitySettings.ServerFullPath;
- options.ApiName = identitySettings.ApiName;
- });
- }
- public void Configure(IApplicationBuilder App)
- {
- // ...
- }
- }
在这个例子中, IdentityServer 实例的基本地址和 API 资源名都是通过强类型选项选项 IdentitySettings 设置的. 这种实现方式在. NET Core 3.0 中已经不再适用了, 所以我们需要一个可替换的方案. 我们可以使用之前提到的方式 - 重新绑定强类型选项或者直接使用 IConfiguration 对象检索配置.
除此之外, 第三种选择是使用 IConfigureOptions, 这是我通过查看 AddIdentityServerAuthentication 方法的底层代码发现的.
事实证明, AddIdentityServerAuthentication()方法可以做一些不同的事情. 首先, 它配置了 JWT Bearer 验证, 并且通过强类型选项指定了验证的方式. 我们可以利用它来延迟配置命名选项(named options), 改为使用 IConfigureOptions 实例.
IConfigureOptions 接口允许你使用 Service Provider 中的其他依赖项延迟配置强类型选项对象. 例如, 如果要配置我的 TestSettings 服务时, 我需要调用 TestService 类中的一个方法, 我可以创建一个 IConfigureOptions 对象实例, 代码如下:
- public class MyTestSettingsConfigureOptions : IConfigureOptions<TestSettings>
- {
- private readonly TestService _testService;
- public MyTestSettingsConfigureOptions(TestService testService)
- {
- _testService = testService;
- }
- public void Configure(TestSettings options)
- {
- options.MyTestValue = _testService.GetValue();
- }
- }
TestService 和 IConfigureOptions<TestSettings > 都是在 Startup.ConfigureServices 方法中同时配置的.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddScoped<TestService>();
- services.ConfigureOptions<MyTestSettingsConfigureOptions>();
- }
这里最重要的一点是, 你可以使用标准的构造函数依赖注入一个 IOptions<TestSettings > 对象. 这里不再需要在 ConfigureServices 方法中 "部分构建"Service Provider, 即可配置 TestSettings. 相反的, 我们注册了配置 TestSettings 的意图, 但是真正的配置会被推迟到配置对象被使用的时候.
那么这对于我们配置 IdentityServer, 有什么帮助呢?
AddIdentityServerAuthentication 使用了强类型选项的一种变体, 我们称之为命名选项(named options). 这种方式在验证配置的时候非常常见, 就像我们上面的例子一样.
简而言之, 你可以使用 IConfigureOptions 方式将验证处理程序使用的命名选项 IdentityServerAuthenticationOptions 的配置延迟. 因此, 你可以创建一个将 IdentitySettings 作为构造参数的 ConfigureIdentityServerOptions 对象.
- public class ConfigureIdentityServerOptions : IConfigureNamedOptions<IdentityServerAuthenticationOptions>
- {
- readonly IdentitySettings _identitySettings;
- public ConfigureIdentityServerOptions(IdentitySettings identitySettings)
- {
- _identitySettings = identitySettings;
- _hostingEnvironment = hostingEnvironment;
- }
- public void Configure(string name, IdentityServerAuthenticationOptions options)
- {
- // Only configure the options if this is the correct instance
- if (name == IdentityServerAuthenticationDefaults.AuthenticationScheme)
- {
- // 使用强类型 IdentitySettings 对象中的值
- options.Authority = _identitySettings.ServerFullPath;
- options.ApiName = _identitySettings.ApiName;
- }
- }
- // This won't be called, but is required for the IConfigureNamedOptions interface
- public void Configure(IdentityServerAuthenticationOptions options) => Configure(Options.DefaultName, options);
- }
在 Startup.cs 文件中, 你需要配置强类型 IdentitySettings 对象, 添加所需的 IdentityServer 服务, 并注册 ConfigureIdentityServerOptions 类, 以便当需要时, 它可以配置 IdentityServerAuthenticationOptions.
- public void ConfigureServices(IServiceCollection services)
- {
- // 配置强类型 IdentitySettings 选项
- services.Configure<IdentitySettings>(Configuration.GetSection("Identity"));
- // 配置 IdentityServer 验证方式
- services
- .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
- .AddIdentityServerAuthentication();
- // 添加其他配置
- services.ConfigureOptions<ConfigureIdentityServerOptions>();
- }
这里, 我们无需向 Startup 类中注入任何内容, 但是你依然可以获得强类型选项的好处. 所以这里我们得到一个双赢的结果.
总结
在本文中, 我描述了升级到 ASP.NET Core 3.0 时, 可以需要对 Startup 类进行的一些修改. 我通过在 Startup 类中注入服务, 描述了 ASP.NET Core 2.x 中的问题, 以及如何在 ASP.NET Core 3.0 中移除这个功能. 最后我展示了, 当需要这种实现方式的时候改如何去做.
来源: https://www.cnblogs.com/lwqlun/p/12194521.html