一、前言
在进行 Web 项目开发的过程中,可能会存在一些需要经常访问的静态数据,针对这种在程序运行过程中可能几乎不会发生变化的数据,我们可以尝试在程序运行前写入到缓存中,这样在系统后续使用时就可以直接从缓存中进行获取,从而减缓因为频繁读取这些静态数据造成的应用数据库服务器的巨大承载压力。
既然需要在程序运行前将静态数据写入到缓存中,毫无疑问我们需要在程序运行前执行一些自定义功能的代码,那么在本章中,我将会介绍如何在 ASP.NET Core 项目中,实现在程序启动前执行某些特定功能的代码。
二、Step by Step
1、先说结论
因为这一篇文章更多的是在说明我在解决这个问题时的一步步思考,并没有涉及到代码的编写,所以下面的内容可能对你的帮助并不是很大,所以这里提前将实现的方式告诉大家。对于这个问题来说,只需要将我们想要执行的代码放到下面代码中注释所在的位置,即可实现我们的需求。
public class Program { public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); // do what you want host.Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
2、前车之鉴
在尝试如何在 ASP.NET Core 中实现这一功能需求前,我们可以看看在 .NET Framework 中如何实现这一功能,是不是可以对我们在后续的功能实现中提供某些借鉴。
对于采用 .NET Framework 的应用程序来说,项目创建后会生成一个 Global.asax 文件,在这个类文件中存在着 Application_Start 这样的一个方法,而 Application_Start 这个方法实际上是在当应用程序接收到第一个 HTTP 请求时触发,也就是说,当系统运行后第一次接收到用户的请求,就会触发 Application_Start 中的代码逻辑,后续不管再接收到多少的请求,都不会再触发该方法。
例如在这个基于 .NET Framework 构建的 MVC 项目模板中,在程序运行前需要执行注册路由信息、注册过滤器、注册使用 bundle 压缩后的 js、css 文件等等。
但是在 ASP.NET Core 项目中,并没有原生存在这样的方法,那么我们如何在 ASP.NET Core 应用中自己动手实现类似的功能呢?
3、后事之师
了解了在之前版本中的实现方式,现在我们仔细看看 Application_Start 这个方法中执行的每行代码的功能,是不是特别像我们在 ASP.NET Core 项目中使用的各种中间件?
然而,如果你有使用过 ASP.NET Core 后就会知道,ASP.NET Core 中的中间件是会在每次请求时都会触发的,虽然我们可以在我们自定义的中间件中设置缓存中不存在数据就写入,存在就直接跳过的代码逻辑,但是既然除了第一次访问时才会真正执行该中间件的功能,后面完全用不到,因此,对于我这种略微强迫症的童鞋来说,这个真的不能忍。。。
既然中间件不可以,而我们需要的仅仅是只运行一次,提到 .NET Core,不知道你的第一印象是什么,对于我个人来说,无处不在的依赖注入,可能是我在 18 年开始学习 .NET Core 时的第一印象。我们知道,对于 .NET Core 中原生的依赖注入组件,存在着三种生命周期:Singleton、Scoped 以及 Transient,对于这三种生命周期的具体解释,还是推荐博客园里蒋金楠老师的一篇文章(电梯直达)。
对于采用 Singleton 方式注入的服务来说,因为是一种类似于全局单例的形式,不管后续从何处进行访问,都会访问的是同一个实例,那么,这里是不是就可以在此基础上实现我们的需求了呢?
很不幸,这里其实是有个很严重的逻辑上的问题的,依赖注入最终的目的是为了实现将我们定义的服务契约与实现进行解耦,实现服务的消费者只需要告诉依赖注入容器自己所需要服务的类型(服务接口 or 抽象服务类),就能自动得到与之匹配的服务实例。
简单点说就是,消费方要告诉服务提供方你要开始使用某个服务了,我才能给你提供对应的服务,就像我们去饭店吃饭,在点了菜后,没有必要关心厨师是用天然气 or 煤气给你烧的菜,但是能不能上菜的关键在于我们有没有点菜。因此,这个问题最终还是落在了我们应该在程序中的什么地方去调用我们设定好的方法。
绕了一圈,似乎我们的想法越来越偏,离我们想要实现的越来越远,既然路偏了,那就直接回到起点吧,抛弃我们在 .NET Framework 项目中的经验,重新从 ASP.NET Core 项目的启动流程开始看起。
在 ASP.NET Core 应用的启动过程中存在着两个非常重要的对象,对应到我们采用的 ASP.NET Core 3.X 的项目中则是 Host 以及 HostBuilder。这里的 Host 就是承载我们 Web 应用运行的载体,而 HostBuilder 则是用来构建 Host 对象的。
PS:因为 ASP.NET Core 3.0 开始加入了 对于 gRPC 框架和 Windows Service 的支持,同时为了与其它非 Web 服务器方案进行集成,因此将原来的 WebHost 和 WebHostBuilder 替换成了新的通用主机(generic-host)配置的模式 。当然,在 3.X 版本你还是可以使用 WebHost 和 WebHostBuilder 的,不过当然是不推荐的。
因为对于 ASP.NET Core 应用程序来说,本质上其实只是一个控制台应用,所以现在我们来看看对于一个控制台应用中最重要的文件:Program.cs, Program 类中的代码如下所示。
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>(); }); }
代码很少,功能也很简单,简单来说,在 Main 方法中构建 HostBuilder 对象,然后去运行它,达到启动我们 Web 应用宿主的目的。
当然,在构建 HostBuilder 对象的过程中,会配置 Kestrel 服务器,会设置 ContentRoot,会加载配置文件等等一系列的动作,因为自己水平太次,尝试了一下,还是解释不好,如果你想要深入了解的话,建议配合博客园里面的这两篇文章一起食用(200行代码,7个对象——让你了解 ASP.NET Core 框架的本质、ASP.NET Core 2.0 : 七.一张图看透启动背后的秘密)。虽然参考文章中都是基于 ASP.NET Core 2.X 版本进行解释说明的,但其实最终的差异不是很大。
不知你是否找到了这个类中对于我们最重要的一点,在 Main 方法中,我们是先构建、再去运行,因此,我们是不是可以在构建完成后,先等一等,把我们想要实现的功能先调用了,再去运行我们的程序。嗯,让我们改造下 Main 方法中的代码。
public class Program { public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); // Get logger // var logger = host.Services.GetRequiredService<ILogger<Program>>(); logger.LogInformation("haha, I ran before web host starting"); host.Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
从上面的图中可以看到,在我们的 Web 应用的宿主程序还未启动之前,控制台就已经打印出了我们自己设定的信息,之后,才是启动我们的 Web 应用,这里是请求我们的 API 接口。同时可以发现,在模拟多次请求时,并不会再次触发我们预设的事件。
三、总结
这一篇文章中并没有包含代码,更多的是针对我之前在开发中遇到的一个问题,自己在解决过程中的一个案例说明,希望可以在你以后遇到这类问题时可以提供一些帮助。离 2020 年的农历新年也没有几天了,按目前的进度,估计就是年前的最后一篇博客了,我也要收拾收拾心情,准备过年了。最后,送大家一张表情包,献给得知你是最后一个放假的童鞋,哈哈哈,提前祝大家新年快乐丫。