IHostingEnvironment VS IHostEnvironment - .NET Core 3.0中的废弃类型

本篇是如何升级到ASP.NET Core 3.0系列文章的第二篇。

  • Part 1 - 将.NET Standard 2.0类库转换为.NET Core 3.0类库
  • Part 2 - IHostingEnvironment VS IHostEnvironment - .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 3.0中已经被标记为废弃的类型。我将解释一下为什么这些类型被废弃了,它们的替换类型是什么,以及你应该什么时候使用它们。

ASP.NET Core与通用主机(Generic Host)合并

在ASP.NET Core 2.1中引入了新的通用主机(Generic Host), 它是借助Microsoft.Extension.*程序集来进行程序配置,依赖注入,以及日志记录来构建非HTTP应用的一种方式。 虽然这是一个相当不错的点子,但是引入主机抽象在基础上与ASP.NET Core使用的HTTP主机不兼容。这导致了多种命名空间的冲突与不兼容,所以在ASP.NET Core 2.x版本中,我一直尽量不使用通用主机。

在ASP.NET Core 3.0中,开发人员作出了巨大的努力,将Web主机与通用主机兼容起来。ASP.NET Core的Web主机现在可以作为IHostedService运行在通用主机中,重复抽象的问题(ASP.NET Core中使用一套抽象,通用主机使用另一套抽象)得到了根本解决。

当然,这还不是全部。当你从ASP.NET Core 2.x升级到3.0, ASP.NET Core 3.0并不强迫你立即使用新的通用主机。如果你愿意,你可以继续使用旧的WebHostBuilder,而不使用新的HostBuilder。虽然在ASP.NET Core 3.0的官方文档中一直暗示这是必须的,但是在当前的阶段,这是一个可选配置,如果你需要,可以继续使用Web主机,而不使用通用主机。

PS: 不过我还是建议你将可能将HostBuilder作为你未来的升级计划。我但是在未来的某个时间点WebHostBuilder将被移除,即使现在它还没有被标记为[Obsolete]

作为重构的通用主机的一部分,一些在之前版本中重复的类型被标记为废弃了,一些新的类型被引入了。在这些类型中,最好的例子就是IHostingEnvironment

IHostingEnvironment VS IHostEnvironment VS IWebHostEnviornment

IHostingEnvironment是.NET Core 2.x中最让人讨厌的一个接口,因为它存在于两个命名空间中, Microsoft.AspNetCore.HostingMicrosoft.Extensions.Hosting.这两个接口有少许不同,且不兼容。

namespace Microsoft.AspNetCore.Hosting
{
    public interface IHostingEnvironment
    {
        string EnvironmentName { get; set; }
        string ApplicationName { get; set; }
        string WebRootPath { get; set; }
        IFileProvider WebRootFileProvider { get; set; }
        string ContentRootPath { get; set; }
        IFileProvider ContentRootFileProvider { get; set; }
    }
}

namespace Microsoft.Extensions.Hosting
{
    public interface IHostingEnvironment
    {
        string EnvironmentName { get; set; }
        string ApplicationName { get; set; }
        string ContentRootPath { get; set; }
        IFileProvider ContentRootFileProvider { get; set; }
    }
}

之所以有两个同名接口是有历史原因的。AspNetCore版本的接口已经存在了很长时间了,在ASP.NET Core 2.1版本中,通用主机引入了Extensions版本。Extensions版本没有提供用于服务静态文件的wwwroot目录的概念(因为它承载的是非HTTP服务)。所以你可能已经注意到Extensions缺少了WebRootFileProviderWebRootPath两个属性。

出于向后兼容的原因,这里需要一个单独的抽象。但是,这种做法真正令人讨厌的后果之一是无法编写用于通用主机和ASP.NET Core的扩展方法。

在ASP.NET Core 3.0中,上述的两个接口都已经被标记为废弃了。你依然可以使用它们,但是在编译的时候,你会得到一些警告。相对的,两个新的接口被引入进来: IHostEnvironmentIWebHostEnvironment。虽然他们出现在不同的命名空间中,但是现在它们有了不同的名字,而且使用了继承关系。

namespace Microsoft.Extensions.Hosting
{
    public interface IHostEnvironment
    {
        string EnvironmentName { get; set; }
        string ApplicationName { get; set; }
        string ContentRootPath { get; set; }
        IFileProvider ContentRootFileProvider { get; set; }
    }
}

namespace Microsoft.AspNetCore.Hosting
{
    public interface IWebHostEnvironment : IHostEnvironment
    {
        string WebRootPath { get; set; }
        IFileProvider WebRootFileProvider { get; set; }
    }
}

这个层次关系更容易理解了,避免了重复,并且意味着接收通用主机版本宿主环境抽象(IHostEnvironment)的方法现在也可以接收web版本(IWebHostEnvironment)的抽象了。在幕后,IHostEnvironmentIWebHostEnvironment的实现是相同的 - 除了旧接口,他们还实现了新接口。

例如,ASP.NET Core的实现类如下:

namespace Microsoft.AspNetCore.Hosting
{
    internal class HostingEnvironment : IHostingEnvironment, 
    		Extensions.Hosting.IHostingEnvironment, 
    		IWebHostEnvironment
    {
        public string EnvironmentName { get; set; } 
                = Extensions.Hosting.Environments.Production;
        public string ApplicationName { get; set; }
        public string WebRootPath { get; set; }
        public IFileProvider WebRootFileProvider { get; set; }
        public string ContentRootPath { get; set; }
        public IFileProvider ContentRootFileProvider { get; set; }
    }
}

那么你到底应该使用哪个接口呢?最简单的答案是"尽可能使用IHostEnvironment接口"。

但是详细来说,情况有很多。。。

如果你正在编写的ASP.NET Core 3.0的应用

尽可能是使用IHostEnviornment接口,但你需要访问WebRootPathWebRootFileProvider两个属性的时候,请使用IWebHostEnvironment接口。

如果你正在编写一个在通用主机和.NET Core 3.0项目中使用的类库

使用IHostEnvironment接口。你的类库依然可以在ASP.NET Core 3.0应用中可用。

如果你正在编写一个在ASP.NET Core 3.0应用中使用的类库

和之前一样,尽量使用IHostEnvironment接口,因为你的类库可能不仅使用在ASP.NET Core应用中,还有可能使用在其他通用主机应用中。然而,如果你需要访问IWebHostEnvironment接口中的额外属性,那么你可能不得不更新你的类库,让它面向netcoreapp3.0,而不是netstandard2.0, 并且添加<FreameworkReference>元素配置。

如果你正在编写一个在ASP.NET Core 2.x和3.0中使用的类库

这种场景比较难处理,基本上你有两种可选的方案:

你可以继续使用Microsoft.AspNetCore版本的IHostingEnvironment。它在2.x和3.0应用中都可以正常工作,你只需要在后续版本中停止使用即可。
使用#ifdef条件编译指令,针对ASP.NET Core 3.0使用IHostEnvironment接口,针对ASP.NET Core 2.x使用IHostingEnviornment接口。
IApplicationLifetime VS IHostApplicationLifetime
IHostingEnvironment接口相似,IApplicationLifetime接口也有命名空间的冲突问题。和之前的例子相同,这两个接口分别存在于Microsoft.Extensions.HostingMicrosoft.AspNetCore.Hosting中。但是在这个例子中,这两个接口是完全一致的。

// 与Microsoft.AspNetCore.Hosting中的定义完全一致
namespace Microsoft.Extensions.Hosting
{
    public interface IApplicationLifetime
    {
        CancellationToken ApplicationStarted { get; }
        CancellationToken ApplicationStopped { get; }
        CancellationToken ApplicationStopping { get; }
        void StopApplication();
    }
}

如你所料,这种重复是向后兼容的征兆。在.NET Core 3.0中新的接口IHostApplicationLifetime被引入,该接口仅在Microsoft.Extensions.Hosting命名空间中定义,但是在通用主机和ASP.NET Core应用中都可以使用。

namespace Microsoft.Extensions.Hosting
{
    public interface IHostApplicationLifetime
    {
        CancellationToken ApplicationStarted { get; }
        CancellationToken ApplicationStopping { get; }
        CancellationToken ApplicationStopped { get; }
        void StopApplication();
    }
}

同样的,这个接口和之前版本是完全一致的。ApplicationLifetime类型在通用主机项目的启动和关闭中扮演了非常重要的角色。非常有趣的是,在Microsoft.AspNetCore.Hosting中没有一个真正等价的类型,Extensions版本的接口处理了两种不同的实现。AspNetCore命名空间中唯一的实现是一个简单的封装类,类型将实现委托给了一个作为通用主机部分被添加的ApplicationLifetime对象中。

namespace Microsoft.AspNetCore.Hosting
{
    internal class GenericWebHostApplicationLifetime : IApplicationLifetime
    {
        private readonly IHostApplicationLifetime _applicationLifetime;
        public GenericWebHostApplicationLifetime(
        	IHostApplicationLifetime applicationLifetime)
        {
            _applicationLifetime = applicationLifetime;
        }

        public CancellationToken ApplicationStarted => 
        	_applicationLifetime.ApplicationStarted;
        public CancellationToken ApplicationStopping =>
        	_applicationLifetime.ApplicationStopping;
        public CancellationToken ApplicationStopped => 
        	_applicationLifetime.ApplicationStopped;
        public void StopApplication() =>
        	_applicationLifetime.StopApplication();
    }
}

幸运的是,选择使用哪一个接口,比选择托管环境(Hosting Environment)要简单的多。

如果你正在编写一个.NET Core 3.0或者ASP.NET Core 3.0应用或者类库

使用IHostApplicationLifetime接口。你只需要引用Microsoft.Extensions.Hosting.Abstractions, 即可以在所有应用中使用。

如果你在编写一个被ASP.NET Core 2.x和3.0应用共同使用的类库

现在,你可能又会陷入困境:

你可以继续使用Microsoft.Extensions版本的IApplicationLifetime。它在2.x和3.0应用中都可以正常使用,但是在未来的版本中,你将不得不停止使用它
使用#ifdef条件编译指令,针对ASP.NET Core 3.0使用IHostApplicationLifetime接口,针对ASP.NET Core 2.x使用IApplicationLifetime接口。
幸运的是,IApplicationLifetime接口通常使用的比IHostingEnvironment接口少的多,所以你可能不会在此遇到过多的困难。

IWebHost VS IHost
这里有一件事情可能让你惊讶,IWebHost接口没有被更新,它没有继承ASP.NET Core 3.0中的IHost。相似的,IWebHostBuilder也没有继承自IHostBuilder。它们依然是完全独立的接口, 一个只工作在ASP.NET Core中,一个只工作在通用主机中。

幸运的是,这也没有关系。现在ASP.NET Core 3.0已经被重构使用通用主机的抽象接口, 你可以编写使用通用主机IHostBuilder抽象的方法,并在ASP.NET Core和通用主机应用中共享它们。如果你需要进行ASP.NET Core的特定操作,你可以依然使用IWebHostBuilder接口。

例如,你可以编写如下的扩展方法,一个使用IHostBuilder, 一个使用IWebHostBuilder:

public static class ExampleExtensions
{
    public static IHostBuilder DoSomethingGeneric(this IHostBuilder builder)
    {
        // 添加通用主机配置
        return builder;
    }

    public static IWebHostBuilder DoSomethingWeb(this IWebHostBuilder builder)
    {
        // 添加Web托管配置
        return builder;
    }
}

其中一个方法在通用主机上进行某些配置(列入,使用依赖注入注册某些服务),在另外一个方法中对IWebHostBuilder进行某种配置,例如你可能会为Kestrel服务器设置一些默认值。

如果你在创建了一个全新的ASP.NET Core 3.0应用,你的Program.cs文件看起来应该是如下代码:

public class Program
{
    public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .UseStartup<Startup>();
            });
}

你可以添加针对两个扩展方法的调用。一个在通用IHostBuilder上调用,另一个在ConfigWebHostDefaults()方法中,针对IWebHostBuilder调用

public class Program
{
    public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .DoSomethingGeneric() // IHostBuilder扩展方法
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .DoSomethingWeb() // IWebHostBuilder扩展方法
                    .UseStartup<Startup>();
            });
}

在ASP.NET Core 3.0中,你可以对两种构建器类型进行调用,这意味着,你现在可以仅依赖通用主机的抽象,就可以在ASP.NET Core应用中复用它们。然后,你可以将ASP.NET Core的特性行为放在顶层,而不必像2.x中一样重复方法。

总结

在本文中,我们讨论了ASP.NET Core 3.0中一些被标记为废弃的类型,它们被移动到哪里去了,以及这么做的原因。如果你正在将一个应用升级到ASP.NET Core 3.0, 你并不需要马上替换它们,因为他们现在的行为依然相同,但是在将来的版本中会被替换掉,因此如果可以的话,最好对其进行更新。在某些场景中,它还使你的应用之间共享代码更加容易,因此值得研究一下。


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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值