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


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

  • 相关阅读:
    [自定义服务器控件] 第三步:CheckBoxList。
    Flex构建WebService应用
    ServletActionContext.getRequest().getServletPath()错误的解决方法
    MyEclipse 8.6 安装 SVN 插件
    [转]hibernateHQL总结
    Struts入门实例
    错误:“Cannot load JDBC driver class 'com.mysql.jdbc.Driver”的解决方法
    Target runtime Apache Tomcat v6.0 is not defined.错误解决方法
    错误:“已有打开的与此命令相关联的 DataReader,必须首先将它关闭”的解决方法。
    [转]删除Windows 7 C:/Users/用户名/AppData里面的文件
  • 原文地址:https://www.cnblogs.com/lwqlun/p/12194521.html
Copyright © 2020-2023  润新知