ASP.NET Core 3.x启动时运行异步任务一

这是一个大的题目,需要用几篇文章来说清楚。这是第一篇。

一、前言

在我们的项目中,有时候我们需要在应用程序启动前执行一些一次性的逻辑。比方说:验证配置的正确性、填充缓存、或者运行数据库清理/迁移等。

如何合理、有效、优雅地完成这个任务,是这个文章讨论的主要内容。

要实现这样一个功能,其实我们有几个选择:

  1. 使用IStartupFilter运行同步任务。这是一个内置的解决方案,可以通过一些设置和技巧来运行异步任务;
  2. 使用IStartupFilterIApplicationLifetime事件来运行异步任务,这是一个可选的方案,但有不足,我们会在后面讲;
  3. 使用IHostedService,在不阻塞应用启动的情况下,运行一些一次性的任务;(关于这个内容,我在前一篇文章ASP.NET Core 3.x控制IHostedService启动顺序浅探中有涉及到一部分内容)
  4. Program.cs中运行异步任务。在大多数情况下,从代码的复杂度到效率上,这都是一个比较好的选择。

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

先提个问题:为什么要在应用启动时运行任务?

二、为什么要在应用启动时运行任务?

在应用启动并开始请求服务之前,很多时候需要运行各种初始化工作。

一个ASP.NET应用启动时,需要完成很多事,例如:

  • 确定当前的宿主环境
  • 加载appsetting.json配置和环境变量
  • 配置并创建依赖注入的容器
  • 配置中间件管道

这是应用启动时要完成的引导内容。

在完成这些内容,运行WebHost并开始监听请求之前,还会有一些一次性任务需要启动,例如:

  • 检查强类型配置的有效性
  • 填充或恢复缓存
  • 数据库清理/迁移(通常来说这不是个好主意,但很多时候没有别的办法)

当然,有些任务也不是一定要在开始监听请求之前运行,这要看具体的运行任务的架构。一般来说,如果缓存处理的完善,是不需要提前启动的。当然,清理/迁移数据库,是必须放在服务启动之前。

在微软官网上,有一个例子是数据保护子系统,用于即时加密(cookie、防伪令牌等),这个就必须在应用监听请求之前完成初始化并加载,这个例子使用了IStartupFilter

三、使用IStartupFilter运行同步任务

IStartupFilters作为配置中间件管道的一部分,通常在Startup.Configure()中运行。它允许我们定制应用的中间件管道,处理我们希望进行的所有任务。

看一个简单的例子:

public class AutoRequestServicesStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<RequestServicesContainerMiddleware>();
            next(builder);
        };
    }
}

IStartupFilter提供了一种可能,在依赖注入容器配置完成之后、应用程序启动之前运行一些代码。因此,我们可以在IStartupFilters中直接使用依赖注入。这表示我们可以运行有关系统的任何代码。在前边提到的微软官网的例子中,就是创建了一个基于IStartupFiltersDataProtectionStartupFilter来初始化数据保护子系统。

此外,IStartupFilter允许我们通过向依赖注入容器注册服务来增加要执行的任务。这是一个很有用的特性,表示我们可以注册一个在应用启动时运行的任务,而不需要显式的调用。

但是,这儿有个问题。IStartupFilters通常运行的是同步的任务。看一下上面的代码,Configure()方法不返回任务。当然,我们硬要使用异步也是可以的,但一般来说,这不算个好主意。原因我后面会写。

写到这儿,如果对ASP.NET Core架构熟悉,就会引出另一个问题:为什么不用健康检查来确认一次性任务的执行结果?

四、为什么不用健康检查?

运行健康检查,是ASP.NET Core 2.2新引入的一个特性,允许查询通过API(HTTP Endpoint)公开的应用的健康状况。当应用部署在Kubernetes,或反向代理HAProxyNginx后面时,可以提供给代理用来检测应用是否准备好开始提供服务。

我们可以使用健康检查来确保应用所有必需的一次性任务完成之前不会开始监听服务。

但是,这种方式会有一点问题。

WebHostKestrel本身会在一次性任务执行前启动。当然,这时他们还不会接收和处理服务请求,但仍然引出了一些问题:

首先是增加了代码的复杂性。除了一次性任务的代码外,还要增加健康检查来测试任务是否完成,并同步和保持任务的状态;其次,如果任务失败了,应用程序的健康检查将会让应用后续的任务无法继续执行。合理的流程是:应用应该立即失败返回。

这儿主要的原因是:健康检查没有定义如何实际运行任务,而只是定义了任务是否成功完成。相对来说,这种状态机制比较单一,在一些简单的任务中可能适用,但不能全面覆盖一次性任务的全部场景。

五、运行异步任务

前边写了一些不太完美的方法。

现在,我们开始进入运行异步方法的一些步骤。当然,运行异步也会有几种方式,适用性上会有一定的区别。

方式1:使用IStartupFilter

前边说过,使用IStartupFilter时,执行的是同步任务。所以,我们可以通过GetAwater().GetResult()来调用异步。

我们拿数据迁移来举个例子。在EF Core中,通过myDBContext.database.migrateasync()在运行时进行数据库迁移。其中,myDBContext是应用程序中DBContext的一个实例。

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

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

            myDbContext.Database.MigrateAsync()
                .GetAwaiter()
                .GetResult();
        }

        return next;
    }
}

通常,GetAwaiter().GetResult()要注意避免死锁的问题。但这儿可能不需要,因为这个代码只在启动时运行,这时候还没有需要处理的请求,所以不太会死锁。

只能说,这样可以用。不过习惯上我会避免这么做。

方式2:使用IApplicationLifetime事件

这是另一个选择。可以通过IApplicationLifetime事件,在应用启动和关闭时接收通知,处理任务。

但这个方式也有局限性。

首先,IApplicationLifetime使用cancellationtoken来注册回调,也就是说,这又是一个同步方式,又需要使用GetAwaiter().GetResult()来调用异步。

其次,ApplicationStarted事件是在WebHost启动之后才会触发,因此异步任务也是在应用开始监听请求后才运行。

方式3:使用IHostedService

IHostedService可以让ASP.NET Core应用在后台执行长时间的任务。

一般来说,IHostedService用在周期性任务、消息传递等任务上,但实际上它并不限于运行这些任务。在ASP.NET Core 3.x上,WebHost本身也是建立在IHostedService上的。

而且,IHostedService本身就是异步的,它提供了StartAsyncStopAsync

这种方式下,我们的代码会是这样:

MigratorHostedService: IHostedService
{
    MigratorStartupFilter(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    
{
        using(var scope = _seviceProvider.CreateScope())
        {
            var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            await myDbContext.Database.MigrateAsync();
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    
{
        return Task.CompletedTask;
    }
}

根据例子可以看出,IHostedService可以直接运行异步任务。

但是,IHostedService也有局限性。从微软官网的说明来看,IHostedService实现期望StartAsync能相对较快的返回。对于后台任务,倾向于异步启动,但主要任务在启动后执行。

在上面这个例子中,数据迁移本身不是问题,但这个长时任务会阻止其它`IHostedService启动和运行。而且,应用会在IHostedService完成数据迁移前开始监听并响应请求,这是一个严重的问题。

方式4:在Program.cs中运行

上面三个方式,都可以解决启动时运行异步任务的问题,但都不够完美,要么要求使用同步(异步转同步可以用,但有隐藏问题),要么不能阻止应用启动,会造成应用启动完成后,可能异步任务还未完成的情况。

我在前边的博文中写到过关于Program.cs中运行IHostedService的方式。具体可以去看ASP.NET Core 3.x控制IHostedService启动顺序浅探

看一下Program.cs的默认代码:

Program
{
    public static void Main(string[] args)
    
{
        CreateWebHostBuilder(args).Build().Run();
    }

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

Build()创建WebHost之后,调用Run()之前,完全可以加入我们需要的代码。同时,C# 7.1后主函数可以改为异步运行。

因此,我们可以在这儿做些文章:

static async Task 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();
    }

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

这个方案的好处是:

  • 这是真正的异步;
  • 任务完成后,应用程序才可以监听并接受请求;
  • 此时已经构建了依赖注入容器,所以可以创建服务;

当然,同样也会有不足:这儿只是构建了DI容器,但并没有建立管道(管道在Run()RunAsync()后才建立,然后是IStartupFilters执行,再然后是应用程序启动)。因此异步任务不能使用管道、IStartupFilters中的配置。不过,这种需求的情况很少。

六、总结

这个部分牵扯到的框架内容比较多。

我们从应用启动时异步运行任务开始,说到了必要性,也说到了几种解决方法,及各自的优缺点。

下一篇文章,我会用一些具体的例子,来说清楚这个方式的具体使用,敬请关注。

(未完待续)

 

 

 

微信公众号:老王Plus

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便