避免在 ASP.NET Core 3.0 中为启动类注入服务

本篇是如何升级到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 开发应用的开发人员,这意味着什么呢?在目前这个阶段,我已经迁移了多个应用,到目前为止,一切都进展顺利。官方的迁移指导文档[1]可以很好的指导你完成所需的步骤,因此,我强烈建议你读一下这篇文档。

在迁移过程中,我遇到的最多两个问题是:

  • ASP.NET Core 3.0 中配置中间件的推荐方式是使用端点路由(Endpoint Routing)。

  • 通用主机不允许为Startup类注入服务

其中第一点,我之前已经讲解过了。端点路由(Endpoint Routing)是在 ASP.NET Core 2.2 中引入的,但是被限制只能在 MVC 中使用。在 ASP.NET Core 3.0 中,端点路由已经是推荐的终端中间件实现了,因为它提供了很多好处。其中最重要的是,它允许中间件获取哪一个端点最终会被执行,并且可以检索有关这个端点的元数据(metadata)。例如,你可以为健康检查端点应用授权。

端点路由是在配置中间件顺序时需要特别注意。我建议你再升级你的应用前,先阅读一下官方迁移文档[2]针对此处的说明,后续我将写一篇博客来介绍如何将终端中间件转换为端点路由。

第二点,是已经提到了的将服务注入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().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(config.GetSection("ConnectionStrings"));services.AddSingleton(ctx => ctx.GetService>().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(options =>options.UseSqlServer(ConnectionStrings.BloggingDatabase));}public void Configure(IApplicationBuilder app){}}
}

我发现,当我先要在ConfigureServices方法中使用强类型选项对象配置其他服务时,这种模式非常的有用。在我上面的例子中,ConnectionStrings对象是一个强类型对象,并且这个对象在程序进入Startup之前,就已经进行非空验证。这并不是一种正规的基础技术,但是实时证明使用起来非常的顺手。

PS:如何为 ASP.NET Core 的强类型选项对象添加验证[3]

然而,如果切换到 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.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类的构造函数注入IHostEnvironmentIConfiguration, 但是仅此而已。至于原因,应该是之前的实现方式会带来一些问题,下面我将给大家详细描述一下。

注意:如果你坚持在 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();}else{services.AddScoped();}}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){// 为依赖注入容器,配置IdentitySettingservices.Configure(Configuration.GetSection("Identity"));// 注册不同的实现services.AddScoped();services.AddScoped();// 根据IdentitySetting配置,在运行时返回一个正确的实现services.AddScoped(ctx =>{var identitySettings = ctx.GetRequiredService();return identitySettings.UseFakeIdentity? ctx.GetRequiredService(): ctx.GetRequiredService();}});}public void Configure(IApplicationBuilder app){// ...}
}

这个实现显然比之前的版本要复杂的多,但是至少可以兼容通用主机的方式。

实际上,如果仅需要一个强类型选项,那么这个方法就有点过头了。相反的,这里我可能只会重新绑定一下配置:

public class Startup
{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){// 为依赖注入容器,配置IdentitySettingservices.Configure(Configuration.GetSection("Identity"));// 重新创建强类型选项对象,并绑定var identitySettings = new IdentitySettings();Configuration.GetSection("Identity").Bind(identitySettings)// 根据条件配置正确的服务if(identitySettings.UseFakeIdentity){services.AddScoped();}else{services.AddScoped();}}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){// 针对依赖注入容器,配置ConnectionStringsservices.Configure(Configuration.GetSection("ConnectionStrings"));// 直接获取配置,不使用强类型选项var connectionString = Configuration["ConnectionString:BloggingDatabase"];services.AddDbContext(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
{private readonly TestService _testService;public MyTestSettingsConfigureOptions(TestService testService){_testService = testService;}public void Configure(TestSettings options){options.MyTestValue = _testService.GetValue();}
}

TestServiceIConfigureOptions都是在Startup.ConfigureServices方法中同时配置的。

public void ConfigureServices(IServiceCollection services)
{services.AddScoped();services.ConfigureOptions();
}

这里最重要的一点是,你可以使用标准的构造函数依赖注入一个IOptions对象。这里不再需要在ConfigureServices方法中“部分构建”Service Provider, 即可配置TestSettings. 相反的,我们注册了配置TestSettings的意图,但是真正的配置会被推迟到配置对象被使用的时候。

那么这对于我们配置 IdentityServer,有什么帮助呢?

AddIdentityServerAuthentication使用了强类型选项的一种变体,我们称之为命名选项(named options). 这种方式在验证配置的时候非常常见,就像我们上面的例子一样。

简而言之,你可以使用IConfigureOptions方式将验证处理程序使用的命名选项IdentityServerAuthenticationOptions的配置延迟。因此,你可以创建一个将IdentitySettings作为构造参数的ConfigureIdentityServerOptions对象。

public class ConfigureIdentityServerOptions : IConfigureNamedOptions
{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 instanceif (name == IdentityServerAuthenticationDefaults.AuthenticationScheme){// 使用强类型IdentitySettings对象中的值options.Authority = _identitySettings.ServerFullPath;options.ApiName = _identitySettings.ApiName;}}// This won't be called, but is required for the IConfigureNamedOptions interfacepublic void Configure(IdentityServerAuthenticationOptions options) => Configure(Options.DefaultName, options);
}

Startup.cs文件中,你需要配置强类型IdentitySettings对象,添加所需的 IdentityServer 服务,并注册ConfigureIdentityServerOptions类,以便当需要时,它可以配置IdentityServerAuthenticationOptions.

public void ConfigureServices(IServiceCollection services)
{// 配置强类型IdentitySettings选项services.Configure(Configuration.GetSection("Identity"));// 配置IdentityServer验证方式services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme).AddIdentityServerAuthentication();// 添加其他配置services.ConfigureOptions();
}

这里,我们无需向Startup类中注入任何内容,但是你依然可以获得强类型选项的好处。所以这里我们得到一个双赢的结果。

总结

在本文中,我描述了升级到 ASP.NET Core 3.0 时,可以需要对Startup 类进行的一些修改。我通过在Startup类中注入服务,描述了 ASP.NET Core 2.x 中的问题,以及如何在 ASP.NET Core 3.0 中移除这个功能。最后我展示了,当需要这种实现方式的时候改如何去做。

参考资料

[1]

迁移指导文档: https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio

[2]

官方迁移文档: https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#routing-startup-code

[3]

如何为ASP.NET Core的强类型选项对象添加验证: https://www.cnblogs.com/lwqlun/p/10084047.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312241.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++set容器-内置类型指定排序

set容器排序 利用访函数&#xff0c;可以改变排序规则 一、set存放内置数据类型 代码如下&#xff1a; #include <iostream> using namespace std; #include <set>class Mycompare {public:bool operator()(int v1, int v2) {return v1 > v2;} };//set容器排序…

workman php 安装,workerman安装及遇到的问题解决

下面由workerman入门教程栏目给大家介绍workerman安装及遇到的问题解决方法&#xff0c;希望对需要的朋友有所帮助&#xff01;1、workerman安装workerman是php的一个socket框架&#xff0c;简化了socket编程&#xff0c;已经为很多企业所用&#xff0c;今天在centos的ngixphp下…

.NET CORE(C#) WPF亚克力窗体

微信公众号&#xff1a;Dotnet9&#xff0c;网站&#xff1a;Dotnet9&#xff0c;问题或建议&#xff1a;请网站留言&#xff0c; 如果对您有所帮助&#xff1a;欢迎赞赏。.NET CORE(C#) WPF亚克力窗体阅读导航本文背景代码实现本文参考源码1. 本文背景本文介绍使用FluentWPF控…

C++map容器-构造和赋值

map基本概念 map构造和赋值 功能描述&#xff1a; 对map容器进行构造和赋值操作 函数原型&#xff1a; 代码如下&#xff1a; #include <iostream> using namespace std; //map容器 构造和赋值 #include <map>void printMap(map<int, int> &m) {fo…

ABP框架v2.0 和 ABP商业版

ABP框架2.0版已经在本周公布.这篇文章解释了为什么我们发布了一个抢先主版本,和2.0版本中的变化.除了v2.0版本,我们很高兴地宣布ABP商业版,这是建立在开源ABP框架的之上的一套专业的模块,工具,主题和服务.ABP框架V2.0为什么2.0,而不是1.2&#xff1f;本来在V1.1.2发布后计划发布…

C++map容器-大小和互换

map大小和互换 功能描述&#xff1a; 统计map容器大小以及交换map容器 函数原型&#xff1a; 代码如下&#xff1a; #include <iostream> using namespace std; #include <map>//map容器 大小和交换 void test01() {map<int, int>m;m.insert(pair<in…

分享一些提高逻辑能力的心得

大家好&#xff0c;我是Z哥。最近有个很明显的感受。就是随着年龄的增长&#xff0c;我越来越发现曾经一些我认为理所应当的事情&#xff0c;在现在看来是如此的傻&#xff0c;如此的没有逻辑。有一句话是这么说的。花半秒钟就看透事物本质的人&#xff0c;和花一辈子都看不清事…

php 配置远程调试,PHP Linux 环境下搭建 XDEBUG并配置远程调试

在网站 https://xdebug.org/download.php 找到对应PHP版本的XDEBUG下载&#xff0c;下载时选择source版本image获取下载地址## 下载打包的源码wget https://xdebug.org/files/xdebug-2.5.5.tgz解包tar zxvf xdebug-2.5.5.tgz进入解包后的源码目录cd xdebug-2.5.5执行 phpize 生…

CAP原理

定义在一个分布式系统&#xff08;指系统中的节点互相连接并共享数据&#xff09;中&#xff0c;当涉及读写操作时&#xff0c;只能保证一致性 (Consistency)、可用性 (Availability)、分区容错性 (Partition Tolerance)三者中的两个&#xff0c;另外一个必须被牺牲。一致性&am…

大学生开题报告php,php毕业设计开题报告

php毕业设计开题报告该频道涵盖有关于开题报告和毕业设计的论文范例,免费教你怎么写php毕业设计论文提供相关参考资料。一、研究背景1 基于英语新课程标准的要求。《英语课程标准》指出&#xff1a;必须正视学生外语学习基础和发展要求的差异&#xff0c;遵循外语学习的客观规律…

服务器应用服务为何卡顿?原来是内存耗尽惹的祸!

做过运维的朋友们都可能会遇到&#xff1a;服务器应用程序运行慢的问题&#xff0c;最终各部门找上门&#xff0c;。今天在这我跟大家分享一个案例及其解决办法。以前我跟很多朋友们也为这样的事情苦恼过&#xff0c;我的服务器内存可是64G啊&#xff0c;这都不够它吃的。后来我…

matlab 传感器的迟滞,MATLAB PI迟滞模型问题

之前我用matlab进行迟滞模型建模&#xff0c;误差非常大&#xff0c;我之前怀疑是自己的参数辨识出问题了&#xff0c;所以我用论文中的阈值向量和权重向量进行建模&#xff0c;输入数据也是用论文数据&#xff0c;但是出来的拟合的线误差非常大&#xff0c;和文献中相差太大。…

(2)MongoDB副本集自动故障转移原理(含客户端)

前文我们搭建MongoDB三成员副本集&#xff0c;了解集群基本特性&#xff0c;今天我们围绕下图聊一聊背后的细节。默认搭建的副本集均在主节点读写&#xff0c;辅助节点冗余部署&#xff0c;形成高可用和备份&#xff0c;具备自动故障转移能力。集群心跳保活集群每个节点以周期性…

matlab如何使用cu文件,Matlab编译cuda的.cu文件

matlab函数&#xff0c;大体首先是用nvcc命令生成.o文件&#xff0c;然后用mex链接对应库文件&#xff0c;生成动态链接库(.mexw64等)。测试环境&#xff1a;1) Windows x64 matlab cuda 5.5 vs20122) Ubuntu 12.04 amd64 server matlab gcc cuda 5.5windows用户需要根据…

C++map容器-查找和统计

map查找和统计 功能描述&#xff1a; 对map容器进行查找数据以及统计数据 函数原型&#xff1a; 代码如下&#xff1a; #include <iostream> using namespace std; #include <map> //map容器 查找和统计void test01() {//查找map<int, int >m;m.insert(p…

C++map容器-插入和删除

map插入和删除 功能描述&#xff1a; map容器进行插入数据和删除数据 函数原型&#xff1a; 代码如下&#xff1a; #include <iostream> using namespace std; #include <map>//map容器 插入和删除 void printMap(map<int, int> &m) {for (map<in…

你需要了解的 JIT Debugging

如果你还不清楚什么是转储文件&#xff0c;不知道什么时候需要转储文件&#xff0c;请参考转储文件系列文章的第一篇 —— 转储文件知多少。前言 我在 你需要知道的 N 种抓取 dump 的工具 的工具 这篇文章里&#xff0c;向大家介绍了几款可以抓取转储文件的工具及其简单用法。不…

超越“迁移”的思考:应用程序该如何被Kubernetes接管?

传统虚拟机&#xff08;VM&#xff09;的可扩展性差强人意&#xff0c;但Kubernetes可以快速&#xff0c;无缝地扩展正在运行的服务。Kubernetes将容器、集群以及广泛的抽象配置方法引入桌面&#xff0c;用于提升部署和变更管理体验&#xff0c;从而使人们对Kubernetes关注更多…

C++string容器-字符串拼接

string字符串拼接 功能描述&#xff1a; 实现在字符串末尾拼接字符串 函数原型&#xff1a; 代码如下&#xff1a; #include <iostream> using namespace std; #include <cstring>//string字符串拼接 void test01() {string str1 "我";str1 "爱…

如何运用领域驱动设计 - 存储库

概述在上一篇文章<如何运用领域驱动设计 - 聚合>中&#xff0c;我们已经了解过领域驱动设计中一个很核心的对象-聚合。在现实场景中&#xff0c;我们往往需要将聚合持久化到某个地方&#xff0c;或者是从某个地方创建出聚合。此时就会使得领域对象与我们的基础架构产生紧…