Lập lịch công việc nền là phần cơ bản của nhiều ứng dụng .NET – cho dù là gửi email, xử lý báo cáo hay đồng bộ dữ liệu.
Trong nhiều năm, các nhà phát triển đã dựa vào các công cụ như Quartz.NET và Hangfire.
Mặc dù đây là những công cụ tuyệt vời, nhưng chúng có một số nhược điểm.
Mục lục
Nhược điểm của Quartz.NET:
- Đăng ký job dựa trên Reflection
- API phức tạp
- Cấu hình nặng nề
- Truyền tham số vào job vụng về
- Không có tích hợp EF Core tích hợp sẵn (chỉ qua package bên thứ 3)
- Không có dashboard tích hợp để giám sát job (chỉ qua package bên thứ 3)
- Không có retry tích hợp
Nhược điểm của Hangfire:
- Đăng ký job dựa trên Reflection
- Hỗ trợ DI hạn chế
- Hỗ trợ async hạn chế
- Một số nhà cung cấp lưu trữ tích hợp yêu cầu giấy phép thương mại
TickerQ là một cách tiếp cận mới cho việc lập lịch job trong .NET – cung cấp API hiện đại, sạch sẽ, persistence tích hợp, tích hợp EF Core và dashboard đẹp mắt để giám sát job.
Nó loại bỏ nhiều boilerplate và nghi thức mà chúng ta đã quen thuộc, thay vào đó cung cấp trải nghiệm thân thiện với nhà phát triển, code-first.
Trong bài viết này, chúng ta sẽ khám phá:
- Cách thiết lập TickerQ trong dự án .NET
- Tích hợp TickerQ với EF Core
- Tạo và lập lịch job (định kỳ và một lần)
- Đăng ký job động
- Sử dụng TickerQ Dashboard để giám sát thực thi job
Bắt Đầu Với TickerQ
TickerQ có sẵn dưới dạng package NuGet nhắm mục tiêu .NET Standard 2.1 và tương thích với bất kỳ ứng dụng .NET Core 3.1+ nào. Nó hoạt động ngay lập tức với ASP.NET Core và không yêu cầu bất kỳ file cấu hình bổ sung nào – mọi thứ đều là code-first và thân thiện với dependency-injection.
Thực hiện theo các bước sau để bắt đầu với TickerQ:
Bước 1: Cài Đặt Package NuGet
dotnet add package TickerQ
Điều này thêm thư viện TickerQ cốt lõi. Bạn có thể tùy chọn cài đặt các package khác như TickerQ.EntityFrameworkCore và TickerQ.Dashboard (chúng ta sẽ đề cập trong các phần tiếp theo).
Bước 2: Đăng Ký TickerQ trong Program.cs
TickerQ cắm trực tiếp vào pipeline khởi động ứng dụng của bạn. Thêm các dòng sau trong Program.cs của bạn:
builder.Services.AddTickerQ();<br><br>var app = builder.Build();<br>app.UseTickerQ();<br><br>app.Run();
Phương thức AddTickerQ đăng ký tất cả các dịch vụ cần thiết cho TickerQ. Phương thức UseTickerQ thêm middleware xử lý các request đến.
Bước 3: Định Nghĩa Một Job
Job TickerQ chỉ là các phương thức lớp thông thường, và các lớp không cần kế thừa bất kỳ interface hoặc lớp cơ sở nào. Chỉ cần thêm thuộc tính TickerFunction vào bất kỳ phương thức nào trong bất kỳ lớp nào.
Đây là ví dụ về một job tạo báo cáo:
public class CreateReportJob<br>{<br> private readonly ReportDbContext _dbContext;<br><br> public CreateReportJob(ReportDbContext dbContext)<br> {<br> _dbContext = dbContext;<br> }<br><br> [TickerFunction(functionName: "Send Notifications", cronExpression: "0 * * * *")]<br> public async Task CreateReport(TickerFunctionContext tickerContext,<br> CancellationToken cancellationToken)<br> {<br> var report = new Report<br> {<br> Title = $"Scheduled Report - {DateTime.UtcNow:yyyy-MM-dd HH:mm}",<br> Content = $"This is an automatically generated report created at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}",<br> CreatedAt = DateTime.UtcNow<br> };<br><br> _dbContext.Reports.Add(report);<br> await _dbContext.SaveChangesAsync(cancellationToken);<br> }<br>}
Trong ví dụ này, TickerQ sẽ gọi phương thức CreateReport mỗi giờ.
Tham số cronExpression là tùy chọn và có thể được sử dụng để lập lịch các job định kỳ dựa trên biểu thức cron. Nếu bạn bỏ qua nó, bạn có thể cấu hình lịch trình khi đăng ký TickerQ trong DI hoặc chỉ định nó một cách động.
Dependency injection được hỗ trợ ngay lập tức, và bạn có thể chỉ định nhiều phương thức job trong một lớp duy nhất như bạn muốn.
Bước 4: Xử Lý Ngoại Lệ (tùy chọn)
Để xử lý ngoại lệ, bạn có thể triển khai interface ITickerExceptionHandler và đăng ký nó trong DI:
public class TickerExceptionHandler : ITickerExceptionHandler<br>{<br> public async Task HandleExceptionAsync(Exception exception,<br> Guid tickerId, TickerType tickerType)<br> {<br> // logic của bạn...<br> }<br><br> public async Task HandleCanceledExceptionAsync(Exception exception,<br> Guid tickerId, TickerType tickerType)<br> {<br> // logic của bạn...<br> }<br>}
services.AddTicker(opt =><br>{<br> opt.SetExceptionHandler<TickerExceptionHandler>(); <br>});
Tích Hợp TickerQ Với EF Core
Để làm cho TickerQ sẵn sàng cho production, đặc biệt trong các trường hợp ứng dụng của bạn có thể được khởi động lại hoặc mở rộng quy mô trên nhiều instance, bạn sẽ muốn persistence.
EF Core cung cấp cho TickerQ một kho lưu trữ quan hệ cho trạng thái job, lịch sử, khóa và phục hồi.
Đầu tiên, cài đặt EF Core extension cho TickerQ:
dotnet add package TickerQ.EntityFrameworkCore
Trong đăng ký dịch vụ của bạn, kích hoạt tính năng EF Core khi gọi AddTickerQ(...). Ví dụ:
services.AddTickerQ(options =><br>{<br> // Đặt thời gian chờ dự phòng để kiểm tra các job bị bỏ lỡ và thực thi.<br> options.UpdateMissedJobCheckDelay(TimeSpan.FromSeconds(10));<br><br> options.SetInstanceIdentifier("TickerQ");<br><br> // Cấu hình kho lưu trữ hoạt động hỗ trợ EF Core cho metadata, khóa và trạng thái TickerQ.<br> options.AddOperationalStore<ReportDbContext>(efCoreOptions =><br> {<br> // Áp dụng cấu hình model tùy chỉnh chỉ trong quá trình EF Core migrations<br> // (design-time). Model runtime vẫn không bị ảnh hưởng.<br> // efCoreOptions.UseModelCustomizerForMigrations();<br><br> // Khi ứng dụng khởi động, hủy các ticker còn lại ở trạng thái Expired hoặc InProgress (đã kết thúc)<br> // để ngăn chặn thực thi trùng lặp sau sự cố hoặc tắt đột ngột.<br> efCoreOptions.CancelMissedTickersOnAppStart();<br><br> // Các hàm dựa trên cron được định nghĩa được tự động seed trong database theo mặc định.<br> // Ví dụ: [TickerFunction(..., "*/5 * * * *")]<br> // Sử dụng điều này để bỏ qua chúng và giữ seed chỉ ở runtime.<br> efCoreOptions.IgnoreSeedMemoryCronTickers();<br> });<br>});
TickerQ sử dụng EF Core để định nghĩa các bảng/thực thể nội bộ của riêng nó.
Bạn có hai tùy chọn:
1. Sử dụng tùy chỉnh model tích hợp cho migrations
Nếu bạn gọi UseModelCustomizerForMigrations(), các cấu hình thực thể TickerQ được áp dụng tự động trong quá trình migrations. Điều này giữ mọi thứ sạch sẽ.
2. Cấu hình thủ công trong DbContext
Nếu bạn không sử dụng model customizer, bạn cần áp dụng cấu hình thực thể trong OnModelCreating một cách rõ ràng, ví dụ:
public class ReportDbContext : DbContext<br>{<br> public ReportDbContext(DbContextOptions<ReportDbContext> options)<br> : base(options) { }<br><br> public DbSet<Report> Reports { get; set; } = null!;<br><br> protected override void OnModelCreating(ModelBuilder modelBuilder)<br> {<br> base.OnModelCreating(modelBuilder);<br><br> modelBuilder.HasDefaultSchema(DbConsts.SchemaName);<br><br> // Thêm schema TickerQ vào EntityFrameworkCore<br> modelBuilder.ApplyConfiguration(new TimeTickerConfigurations(schema: DbConsts.SchemaName));<br> modelBuilder.ApplyConfiguration(new CronTickerConfigurations(schema: DbConsts.SchemaName));<br> modelBuilder.ApplyConfiguration(new CronTickerOccurrenceConfigurations(schema: DbConsts.SchemaName));<br><br> // ...<br> }<br>}
Sau khi thiết lập, tạo migration EF của bạn như thường lệ với lệnh EF Core CLI:
dotnet ef migrations add InitialMigration -c ReportDbContext
Đăng Ký Job Động
TickerQ hỗ trợ cả job một lần / bị trì hoãn (gọi là TimeTickers) và job định kỳ hoặc dựa trên cron (gọi là CronTickers).
Bạn có thể lập lịch job động qua các endpoint API hoặc trong quá trình khởi động ứng dụng, với các chính sách retry, khoảng thời gian tùy chỉnh và hơn thế nữa.
Giả sử bạn có một endpoint API nơi người dùng yêu cầu gửi thông báo tại một ngày/giờ trong tương lai.
Bạn có thể sử dụng ITimeTickerManager<TimeTicker> để lập lịch một job như vậy:
public record NotificationJobContext(string Title, string Content);<br><br>app.MapPost("/api/schedule‑notification", async (NotificationRequest request, <br> ITimeTickerManager<TimeTicker> timeTickerManager) =><br>{<br> if (request.ScheduledTime <= DateTime.Now)<br> {<br> return Results.BadRequest("Scheduled time must be in the future");<br> }<br><br> // Tạo dữ liệu job được gõ.<br> var jobData = new NotificationJobContext(request.Title, request.Content);<br><br> // Lập lịch job.<br> var result = await timeTickerManager.AddAsync(new TimeTicker<br> {<br> Function = "Send Notifications",<br> ExecutionTime = request.ScheduledTime.ToUniversalTime(),<br> Request = TickerHelper.CreateTickerRequest(jobData),<br> Retries = 3,<br> RetryIntervals = new[] { 30, 60, 120 } // Retry sau 30s, 60s, rồi 2 phút<br> });<br><br> return Results.Ok(new<br> {<br> JobId = result.Result.Id,<br> Message = $"Notification '{request.Title}' scheduled for {request.ScheduledTime}"<br> });<br>});
Ở đây tôi truyền NotificationJobContext vào job và tôi có thể trích xuất nó trong phương thức job:
public class NotificationJob<br>{<br> private readonly ILogger<NotificationJob> _logger;<br> public const string TitleKey = "Title";<br> public const string ContentKey = "Content";<br><br> public NotificationJob(ILogger<NotificationJob> logger)<br> {<br> _logger = logger;<br> }<br><br> [TickerFunction(functionName: "Send Notifications", cronExpression: "0 0 * * *" )]<br> public Task Execute(TickerFunctionContext<NotificationJobContext> tickerContext,<br> CancellationToken cancellationToken)<br> {<br> var title = tickerContext.Request.Title;<br> var content = tickerContext.Request.Content;<br><br> // ...<br><br> return Task.CompletedTask;<br> }<br>}
Ngoài ra, bạn có thể sử dụng ICronTickerManager<CronTicker> để lập lịch một job định kỳ:
var result = await cronTickerManager.AddAsync(new CronTicker<br>{<br> Function = "Send Notifications",<br> Request = TickerHelper.CreateTickerRequest(jobData),<br> Expression = "* * * * *",<br> Retries = 3,<br> RetryIntervals = new[] { 30, 60, 120 } // Retry sau 30s, 60s, rồi 2 phút<br>});
Đây là cách bạn có thể cập nhật và xóa một CronTicker:
// Cập nhật CronTicker bằng ID<br>// Bạn nhận được ID khi lập lịch job (result.Result.Id)<br>await cronTickerManager.UpdateAsync(tickerId, ticker =><br>{<br> ticker.Description = "Updated cron description";<br> ticker.Expression = "*/10 * * * *"; // mỗi 10 phút<br>});<br><br>// Xóa CronTicker bằng ID<br>await cronTickerManager.DeleteAsync(tickerId);
Sử Dụng TickerQ Dashboard
TickerQ có một dashboard UI tích hợp, first-party cung cấp cho bạn khả năng hiển thị và kiểm soát thời gian thực đối với tất cả các job đã lập lịch của bạn (cả dựa trên thời gian và dựa trên cron).
Nó hữu ích để debug, bảo trì và quản lý khối lượng công việc production.
Thêm package NuGet TickerQ.Dashboard vào dự án của bạn.
dotnet add package TickerQ.Dashboard
Đăng ký hỗ trợ dashboard khi cấu hình TickerQ trong DI:
builder.Services.AddTickerQ(options =><br>{<br> // ...<br> <br> options.AddOperationalStore<ReportDbContext>(efCoreOptions =><br> {<br> // ...<br> });<br> <br> options.AddDashboard(x =><br> {<br> x.BasePath = "/tickerq-dashboard";<br> x.EnableBasicAuth = true;<br> });<br>});
TickerQ hỗ trợ Xác thực Cơ bản ngay lập tức cho dashboard UI. Bạn có thể cấu hình thông tin đăng nhập trong appsettings.json của mình:
"TickerQBasicAuth": {<br> "Username": "admin",<br> "Password": "admin"<br>}
Để truy cập dashboard, điều hướng đến https://localhost:5001/tickerq-dashboard trong trình duyệt của bạn (bất cứ cổng nào của bạn).
Nhập admin/admin để đăng nhập.
Tổng Kết
TickerQ là một thư viện lập lịch hiện đại, nhẹ và sẵn sàng cho production cho các ứng dụng .NET – cung cấp một sự thay thế mới mẻ cho Hangfire và Quartz.NET.
Bạn có thể thấy Quartz yêu cầu nhiều code hơn bao nhiêu khi so sánh với TickerQ.
TickerQ cung cấp:
- API hiện đại, sạch sẽ, code-first
- Tích hợp EF Core tích hợp sẵn
- Dashboard đẹp mắt để giám sát job
- Hỗ trợ retry tích hợp
- Dependency injection đầy đủ
- Hỗ trợ async hoàn toàn
- Đăng ký job động linh hoạt
- Không yêu cầu giấy phép thương mại cho các tính năng cốt lõi
Hy vọng bạn thấy bài viết này hữu ích. TickerQ thực sự là một lựa chọn tuyệt vời cho các nhà phát triển .NET đang tìm kiếm một giải pháp lập lịch job hiện đại và hiệu quả.



