zoukankan      html  css  js  c++  java
  • ASP.NET Core 3.x启动时运行异步任务(二)

    这一篇是接着前一篇在写的。如果没有看过前一篇文章,建议先去看一下前一篇,这儿是传送门

    一、前言

    前一篇文章,我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点。最后,还提出了一个比较合理的解决方法:通过在Program.cs里加入代码,来实现IWebHost启动前运行异步任务。

    实现的代码再贴一下:

    public class Program
    {

        public static async Task Main(string[] args)
        
    {
            IWebHost webHost = CreateWebHostBuilder(args).Build();

            using (var scope = webHost.Services.CreateScope())
            {
                var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

                await myDbContext.Database.MigrateAsync();
            }

            await webHost.RunAsync();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }

    这个方法是有效的。但是,也会有一点不足。

    从.Net Core的最简规则来说,我们不应该在Program.cs中加入其它代码。当然,我们可以把这部分代码转到一个外部类中,但最后也必须手动加入到Program.cs中。尤其是在多个应用中,使用相同的模式时,这种方式会很麻烦。

        为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/13714679.html

    也许,我们可以采用向DI容器中注入启动任务?

    二、向DI容器中注入启动任务

    这种方式,是基于IStartupFilterIHostedService两个接口,通过这两个接口可以向依赖注入容器中注册类。

    首先,我们为启动任务创建一个简单接口:

    public interface IStartupTask
    {
        Task ExecuteAsync(CancellationToken cancellationToken = default);
    }

    再建一个扩展方法,用来向DI容器注册启动任务:

    public static class ServiceCollectionExtensions
    {

        public static IServiceCollection AddStartupTask<T>(this IServiceCollection services)
            where T : classIStartupTask
            => services.AddTransient<IStartupTask, T>();

    }

    最后,再建一个扩展方法,在应用启动时,查找所有已注册的IStartupTask,按顺序执行他们,然后启动IWebHost

    public static class StartupTaskWebHostExtensions
    {

        public static async Task RunWithTasksAsync(this IHost webHost, CancellationToken cancellationToken = default)
        
    {
            var startupTasks = webHost.Services.GetServices<IStartupTask>();

            foreach (var startupTask in startupTasks)
            {
                await startupTask.ExecuteAsync(cancellationToken);
            }

            await webHost.RunAsync(cancellationToken);
        }
    }

    这样就齐活了。

    还是用一个例子来看看这个方式的具体应用。

    三、示例 - 数据迁移

    实现IStartupTask其实和实现IStartupFilter很相似,可以从DI容器中注入。如果需要考虑作用域,还可以注入IServiceProvider,并手动创建作用域。

    例子中,数据迁移类可以写成这样:

    public class MigratorStartupFilter: IStartupTask
    {
        private readonly IServiceProvider _serviceProvider;
        public MigratorStartupFilter(IServiceProvider serviceProvider)
        
    {
            _serviceProvider = serviceProvider;
        }

        public async Task ExecuteAsync(CancellationToken cancellationToken = default)
        
    {
            using(var scope = _seviceProvider.CreateScope())
            {
                var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
                await myDbContext.Database.MigrateAsync();
            }
        }
    }

    下面,把任务注入到ConfigureServices()中:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddStartupTask<MigrationStartupTask>();
    }

    最后,用上一节中的扩展方法RunWithTasksAsync()来替代Program.cs中的Run():

    public class Program
    {

        public static async Task Main(string[] args)
        
    {
            // await CreateWebHostBuilder(args).Build().RunAsync();
            await CreateWebHostBuilder(args).Build().RunWithTasksAsync();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }

    从功能上来说,跟上一篇的代码区别不大,但这样的写法,又多了一些优点:

    1. 任务代码放到了Program.cs之外。这符合微软的建议,也更容易理解;
    2. 任务放到了DI容器中,这样更容易添加额外的任务;
    3. 如果没有额外任务,这个代码和标准的Run()一样,所以这个代码可以独立成一个模板。

    简单来说,使用RunWithTasksAsync()后,可以轻松地向DI容器添加额外的任务,而不需要任何其它的更改。

    满意了吗?好像感觉还差一点点…

    四、不够完美的地方

    如果要照着完美去做,好像还差一点点。

    这个一点点是在于:任务现在运行在IConfiguration和DI容器配置完成后,IStartupFilters运行和中间件管道配置完成之前。换句话说,如果任务需要依赖于IStartupFilters,那这个方案行不通。

    在大多数情况下,这没什么问题。以我自己的经验来看,好像没有什么功能需要依赖于IStartupFilters。但作为一个框架类的代码,需要考虑这种情况发生的可能性。

    以目前的方案来说,好像还没办法解决。

    应用启动时,当调用WebHost.Run()时,是内部调用WebHost。看一下StartAsync()的简化代码:

    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    {
        _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();

        var application = BuildApplication();

        _applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
        _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
        var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>();
        var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
        var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory);

        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

        _applicationLifetime?.NotifyStarted();

        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);
    }

    如果我们希望任务是加在BuildApplication()调用和Server.StartAsync()的调用之间,该怎么办?

    这段代码能给出答案:我们需要装饰IServer。 ¨K16K 首先,我们替换IServer的实现: ¨G8G 在这段代码中,我们拦截StartAsync()调用并注入任务,然后回到内置处理。 下面是对应的扩展代码: ¨G9G 这个扩展代码做了两件事:在DI容器中注册了IStartupTask,并装饰了之前注册的IServer实例。装饰方法Decorate()我略过了,有兴趣的可以去了解一下 - 装饰模式。 Program.cs的代码和第三节的代码相同,略过。 &emsp; 我们终于做到了在应用程序完全构建完成后去执行我们的任务,包括IStartupFilters`和中间件管道。

    现在的流程,类似于下面这个微软官方的图:

    (全文完)

     


    微信公众号:老王Plus

    扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送

    本文版权归作者所有,转载请保留此声明和原文链接

  • 相关阅读:
    判断具有某个属性js、jQuery
    新建maven项目,JRE System Library[J2SE-1.5]
    maven多模块搭建
    The POM for * is invalid
    【react】react-bookManager
    【cml】wosi-demo
    关于白盒测试
    【weex】h5weex-example
    【weex】publishTask
    Eslint报错的翻译
  • 原文地址:https://www.cnblogs.com/tiger-wang/p/13714679.html
Copyright © 2011-2022 走看看