原文: https://andrewlock.net/avoiding-startup-service-injection-in-asp-net-core-3/

作者: Andrew Lock

译者: Lamond Lu

本篇是如何升级到ASP.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类的构造函数注入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<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();
}
}

TestServiceIConfigureOptions<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中移除这个功能。最后我展示了,当需要这种实现方式的时候改如何去做。

避免在ASP.NET Core 3.0中为启动类注入服务的更多相关文章

  1. ASP.NET Core 1.0 中的依赖项管理

    var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...

  2. 在ASP.NET Core 1.0中如何发送邮件

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:目前.NET Core 1.0中并没有提供SMTP相关的类库,那么要如何从ASP.NE ...

  3. ASP.NET Core 1.0 中使用 Swagger 生成文档

    github:https://github.com/domaindrivendev/Ahoy 之前文章有介绍在ASP.NET WebAPI 中使用Swagger生成文档,ASP.NET Core 1. ...

  4. 用ASP.NET Core 1.0中实现邮件发送功能

    准备将一些项目迁移到 asp.net core 先从封装类库入手,在遇到邮件发送类时发现在 asp.net core 1.0中并示提供SMTP相关类库,于是网上一搜发现了MailKit 好东西一定要试 ...

  5. 在ASP.NET Core 2.0中使用CookieAuthentication

    在ASP.NET Core中关于Security有两个容易混淆的概念一个是Authentication(认证),一个是Authorization(授权).而前者是确定用户是谁的过程,后者是围绕着他们允 ...

  6. 如何在ASP.NET Core 2.0中使用Razor页面

    如何在ASP.NET Core 2.0中使用Razor页面  DotNetCore2017-11-22 14:49 问题 如何在ASP.NET Core 2.0中使用Razor页面 解 创建一个空的项 ...

  7. ASP.NET Core 3.0中使用动态控制器路由

    原文:Dynamic controller routing in ASP.NET Core 3.0 作者:Filip W 译文:https://www.cnblogs.com/lwqlun/p/114 ...

  8. asp.net core 3.0 中使用 swagger

    asp.net core 3.0 中使用 swagger Intro 上次更新了 asp.net core 3.0 简单的记录了一下 swagger 的使用,那个项目的 api 比较简单,都是匿名接口 ...

  9. 探索 ASP.Net Core 3.0系列三:ASP.Net Core 3.0中的Service provider validation

    前言:在本文中,我将描述ASP.NET Core 3.0中新的“validate on build”功能. 这可以用来检测您的DI service provider是否配置错误. 具体而言,该功能可检 ...

随机推荐

  1. 用diiv实现多个方块居中嵌套--padding

    文章地址 https://www.cnblogs.com/sandraryan/ 案例:用diiv嵌套多个正方形,配合盒模型相关知识,使每个div在他的父元素上居中.(每个div中心点对齐) 涉及到m ...

  2. 手风琴jq实现

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. H3C HDLC概述

  4. pycharm下的多个python版本共存(二)

    上一篇博文介绍了在windows下同时安装python2和python3.而在工作的过程中,我习惯于用pycharm作为IDE.本文将记录如何在pycharm中选择python版本,并给相应的版本安装 ...

  5. 【t046】牛跳

    Time Limit: 1 second Memory Limit: 128 MB [问题描述] John的奶牛们计划要跳到月亮上去.它们请魔法师配制了P(1 <= P <=150,000 ...

  6. 基于小米即时消息云服务(MIMC)的Web IM

    michat 一个基于小米即时消息云服务(MIMC)的Web IM. 源码地址github和gitee同步. 截图展示 如何使用 请先双击目录"需要安装的jars"的install ...

  7. 2019-3-1-获取-Nuget-版本号

    title author date CreateTime categories 获取 Nuget 版本号 lindexi 2019-3-1 9:27:6 +0800 2019-02-25 15:51: ...

  8. 判断是否是ie浏览器或者edge浏览器,引入特定的css

    判断是否是ie浏览器或者edge浏览器,引入特定的css 我本来要用ie浏览器专有的条件注释语句来引入,但是发现都没有效果,网上有说ie10之后的浏览器取消了条件语句,反正我是只要是IE都没有试出效果 ...

  9. dotnet core 输出调试信息到 DebugView 软件

    本文告诉大家如何在 dotnet core 输出调试信息到 DebugView 软件 在之前告诉小伙伴,如何在 WPF 输出调试信息到 DebugView 软件,请看文章 WPF 调试 获得追踪输出 ...

  10. Eclipse GlassFish Server 配置

    一.下载GlassFish Server    通过如下地址下载合适版本:    http://glassfish.java.net/public/downloadsindex.html    htt ...