Asp-Net-Core Development Notes: Integrating Hangfire for Async Task Queues and Scheduled Jobs
Introduction Recently I've been migrating a data collection platform written in Python to .Net Core. Originally the collection tasks used multiprocessing + a thread pool to speed up collection, and used Celery as an async task queue that also handled scheduled tasks. This setup worked okay, but hey, tinkering is the point, so let's just go with C#~ This article records how Hangfire is used in a real application. I found that most articles online just use Hangfire's async jobs to print Hello World, and then that's it. I really don't know what the point of writing such articles
目录14 个章节
Introduction
Recently I've been migrating a data collection platform written in Python to .Net Core. Originally the collection tasks used multiprocessing + a thread pool to speed up collection, and used Celery as an async task queue that also handled scheduled tasks. This setup worked okay, but hey, tinkering is the point, so let's just go with C#~
This article records how Hangfire is used in a real application. I found that most articles online just use Hangfire's async jobs to print Hello World, and then that's it. I really don't know what the point of writing such articles is?? Besides wasting the reader's time, they also waste the author's time writing them...
.NetCore as a high-performance platform naturally shouldn't lose to Python, but I don't want to reinvent the wheel (I'm a noob), so I'll find an existing solution to use, to avoid pitfalls~
First I researched the current .Net Core ecosystem and found a few options:
- FreeScheduler
- Quartz.net
- Hangfire
The first one, FreeScheduler, is from the same author as the FreeSQL project. My project happens to use FreeSQL as the ORM as well, but looking at GitHub it has relatively few stars, and the docs aren't complete yet. The dependency injection support I care about most isn't easy to set up for now, so I had to drop it.
Then between Quartz.net and Hangfire, I chose the latter, because I had used Hangfire when working on the CrawlCenterNet project before. It was pretty good, and it comes with a simple dashboard, which is quite intuitive~
So let's get started~
Choosing a Backend
Here "backend" refers to the storage backend for the task queue, i.e. Storage as described in the Hangfire docs.
I checked the official site — most relational and non-relational databases are supported, so I was relieved. Since the production environment currently uses Oracle, I initially chose Oracle as Storage. But when I ramped up to 2000+ jobs at the same time it threw an error. Looking into it, the reason was insufficient tablespace — an Oracle issue...
So for performance and stability, I dropped it, then tried SQLite (for local testing only). I found that even though I defined a Connection String in the config, it ignored me and used that Connection String as the database name. Speechless... That way I couldn't set the SQLite database to async mode, so the speed was basically a turtle crawl...
Dropped it again... This time I went straight with Redis. For better performance, I gave up persistence~ Redis didn't disappoint me either — thousands of jobs without even batting an eye, nice~
Related Links
- For Oracle backend Storage I used this: https://github.com/akoylu/Hangfire.Oracle.Core
Data Collection Code
The Hangfire component wasn't introduced from the start. I'll first show the most basic data collection code, so the later explanation will be clearer~
The key code is in the Services/CrawlService.cs file
public class CrawlService {
// 依赖注入一些服务
private readonly IBaseRepository<Proc> _repo;
public async Task CrawlAllProc() {
for(var i=1; i<2000; i++) {
await CrawlProcList(i);
}
}
public async Task CrawlProcList(int page) {
// 具体代码省略了
var procList = ; //...
foreach (var proc in procList) {
await CrawlProc(proc);
}
}
public async Task CrawlProc(Proc proc) { }
}
Then, when starting a collection job, we just call the CrawlAllProc method, which starts collecting page by page. Each page has many Proc records, all of which get collected.
The code above uses await, which waits for the async method to complete, so it's very slow. If we remove await, the task runs on a new thread without waiting for it to finish, but the problem is obvious: if something goes wrong it's hard to debug, which makes it difficult to guarantee system stability.
Next we'll refactor this with Hangfire.
Installing Hangfire
This project uses the following dependencies:
- Hangfire.Core
- Hangfire.AspNetCore
- Hangfire.Redis.StackExchange
Just nuget them all in one go and done
Registering Services
To distinguish from the later content, here's the official example first
Registering services:
services.AddHangfire(
configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
// 根据实际使用的 Storage 来注册
.UseRedisStorage();
services.AddHangfireServer();
Adding middleware:
app.UseHangfireDashboard();
Basic Usage
Hangfire is registered as a singleton by default, so you can add async jobs or scheduled jobs from any code using its static methods.
Async jobs:
BackgroundJobs.Enqueue(() => Console.WriteLine("Hello world!"));
Scheduled jobs:
Hangfire's scheduled jobs are called recurrent tasks. I used to usually call them scheduled task, and at first I almost couldn't find the docs~
The following code adds a job that runs once a day. If you need a different schedule, you can customize the Cron parameter that follows. Look up Cron syntax yourself~
RecurringJob.AddOrUpdate("easyjob", () => Console.Write("Easy!"), Cron.Daily);
Proper Usage
OK, finally getting to the main content
As I said at the beginning, the usage introduced above is far from enough. If it were just introducing a Hello World, there would be no need to write a dedicated article...
Now I'll introduce how to use Hangfire together with my CrawlService
Because CrawlService needs to operate on the database, it uses dependency injection, so we need Hangfire to support DI as well.
The official docs have a section on IoC containers (https://docs.hangfire.io/en/latest/background-methods/using-ioc-containers.html), but they don't cover the AspNetCore container. Time to just DIY it~
Adding AspNetCore's DI Container
I searched for a long time at first and couldn't find anything. Eventually I found an example on GitHub. The AspNetCore version in it was really old — it was actually version 1.1, which I've never even used... But that didn't stop me from borrowing their approach~
This step needs a subclass of JobActivator
Let's write one. I put it under the Infrastructure directory
using Hangfire;
public class HangfireActivator : JobActivator {
private readonly IServiceProvider _serviceProvider;
public HangfireActivator(IServiceProvider serviceProvider) {
_serviceProvider = serviceProvider;
}
public override object? ActivateJob(Type jobType) {
return _serviceProvider.GetService(jobType);
}
}
Here, in the HangfireActivator constructor we pass in AspNetCore's IoC container object, and override the ActivateJob method so that when Hangfire activates a job it gets the object from the IoC container. Pretty straightforward.
Modifying the Service Registration Code
Actually the service registration part is the same and doesn't need to be changed
But as a habit, to keep Program.cs or Startup.cs relatively clean, I still wrote an extension method for this part.
Add ConfigureHangfire.cs in the Extensions directory
public static class ConfigureHangfire {
public static void AddHangfirePkg(this IServiceCollection services, IConfiguration configuration) {
services.AddHangfire(conf => conf
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseRedisStorage()
);
services.AddHangfireServer();
}
public static void UseHangfire(this WebApplication app) {
GlobalConfiguration.Configuration.UseActivator(new HangfireActivator(app.Services));
app.UseHangfireDashboard();
}
}
You can see the change is that before adding the middleware, we configure the Activator with this line:
GlobalConfiguration.Configuration.UseActivator(new HangfireActivator(app.Services));
Just pass in the IoC container
Done~
Then in Program.cs (I'm using .Net6), just use this extension method and you're done~
builder.Services.AddHangfirePkg(builder.Configuration);
// 中间件
app.UseHangfire();
Creating Jobs
With DI in place, creating an async job looks like this. There's just an extra generic parameter.
BackgroundJob.Enqueue<CrawlService>(a => a.CrawlAllProc());
Scheduled job, runs once every hour
RecurringJob.AddOrUpdate<CrawlService>(a => a.CrawlAllProc(), Cron.Hourly);
Refactoring the Data Collection Code
OK, finally back to the original data collection code, with the following changes:
public class CrawlService {
// 依赖注入一些服务
private readonly IBaseRepository<Proc> _repo;
public async Task CrawlAllProc() {
for(var i=1; i<2000; i++) {
// await CrawlProcList(i);
BackgroundJob.Enqueue(() => CrawlProcList(i, 100));
}
}
public async Task CrawlProcList(int page, int pageSize = 100) {
// 具体代码省略了
var procList = ; //...
foreach (var proc in procList) {
// await CrawlProc(proc);
BackgroundJob.Enqueue(() => CrawlProc(proc));
}
}
public async Task CrawlProc(Proc proc) { }
}
Comment out the original awaits and replace them with creating async jobs via Hangfire. Run it, open the dashboard, and you can see jobs shoot up to thousands in a flash — extremely fast~
One thing to note is that the second parameter
pageSizeof theCrawlProcListmethod has a default value of 100. In normal use that's fine — you can omit this parameter and it defaults to 100.But in the
BackgroundJob.Enqueuemethod you cannot omit this parameter, otherwise you'll get an error saying the compiler can't resolve something. This is probably a C# language limitation; I haven't looked into it in depth yet.
Wrap-up
OK, that roughly wraps up the data collection & scheduled collection features. This part happened to be finished on the first day of National Day overtime. I'll leave the rest to time~ Let it run to its heart's content over the remaining days of the National Day holiday, and I'll check the results when the holiday is over. If there's new progress I'll update the blog promptly.
Oh right, I also plan to wrap async jobs and scheduled jobs behind an interface (seems like AspNetCore doesn't have this functionality?). I don't want the code to be too tightly coupled with Hangfire. Wrapping it as an abstract interface means switching to another library later won't be a problem.
I'll add this to the todo list for now~
References
_EOF_
Asp-Net-Core Development Notes: Integrating Hangfire for Async Task Queues and Scheduled Jobs
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
Asp-Net-Core开发笔记:FrameworkDependent搭配docker部署
之前我写过一篇使用 docker 部署 AspNetCore 应用的文章,这种方式搭配 CICD 非常方便, build 之后 push 到私有的 dockerhub ,在生产服务器上 pull 下来镜像就可以直接运行了。 然而,有时需要一种更传统的部署方式,比如在本地打包可执行文件之后直接放到服务器上执行。 这种方式配置完成之后,每次更新只需要重新本地重新 publish 后把可执行文件上传
2023-09-08 · Asp-Net-Core开发笔记 · 约 1 分钟
AspNetCore开发笔记:WebApi项目集成企业微信和公众号
微信开发文档烂得离谱,直接用SKIT库搞定。异步处理消息,图片下载内存处理,公众号被动回复5秒内搞定。啧啧。
2025-12-22 · Asp-Net-Core开发笔记 · 约 1 分钟
Asp-Net-Core开发笔记:使用NPM和gulp管理前端静态文件
前言 本文介绍的是AspNetCore的MVC项目,WebApi+独立前端这种前后端分离的项目就不需要多此一举了~默认前端小伙伴是懂得使用前端工具链的。 为啥要用MVC这种服务端渲染技术呢? 简单项目不需要强行分离增加复杂度(如:我正在开发的博客项目) 后端渲染利于SEO,对博客网站友好 OK,虽然MVC的技术老了点,但依然可以结合现代前端工具链来提高效率 本文的食用需要先安装好Node.js环境
2022-02-17 · Asp-Net-Core开发笔记 · 约 1 分钟
Asp-Net-Core开发笔记:使用原生的接口限流功能
试用了一下,感觉还是太折腾,用回原来的 AspNetCoreRateLimit 组件得了
2024-05-22 · Asp-Net-Core开发笔记 · 约 1 分钟
讨论
共 0 条加载评论中…