TCIS.Hangfire
1.0.0
dotnet add package TCIS.Hangfire --version 1.0.0
NuGet\Install-Package TCIS.Hangfire -Version 1.0.0
<PackageReference Include="TCIS.Hangfire" Version="1.0.0" />
<PackageVersion Include="TCIS.Hangfire" Version="1.0.0" />
<PackageReference Include="TCIS.Hangfire" />
paket add TCIS.Hangfire --version 1.0.0
#r "nuget: TCIS.Hangfire, 1.0.0"
#:package TCIS.Hangfire@1.0.0
#addin nuget:?package=TCIS.Hangfire&version=1.0.0
#tool nuget:?package=TCIS.Hangfire&version=1.0.0
TCIS.Hangfire
Đưa IWorkContext đi qua ranh giới job của Hangfire: bắt ngữ cảnh lúc enqueue, dựng lại lúc
thực thi, dọn sạch sau đó. Kèm nối lại W3C trace để log của job nằm cùng một trace với request
đã tạo ra nó.
Gói này không biết gì về multi-tenancy — xem §6.
Mục lục
- Đăng ký
- Ngữ cảnh đi qua ranh giới job như thế nào
- Hợp đồng trên dây: tham số
wc:* - CorrelationId đổi mỗi lần chạy — có chủ đích
- Nối lại trace
- Scope DI và cách ly giữa các job
- Job đa cảng
- Những chỗ dễ vấp
1. Đăng ký
dotnet add package TCIS.Hangfire
using Hangfire;
using TCIS.Hangfire.Extensions;
builder.Services.AddHangfire(config =>
{
config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"));
config.UseTWorkContext();
});
builder.Services.AddHangfireServer();
Đổi danh tính mặc định của job hệ thống:
config.UseTWorkContext(o =>
{
o.DefaultSystemUsername = "svc-billing";
o.DefaultSystemCaller = "hangfire-nightly";
});
Không cần DI.
TWorkContextJobFilterđọc/ghi quaTWorkContextAccessor, mà lớp đó giữ ngữ cảnh trong mộtAsyncLocalstatic — mọi instance dùng chung một ô nhớ. Nhờ vậy bộ lọc dựng được bằngnewvà không cần service provider.
2. Ngữ cảnh đi qua ranh giới job như thế nào
| Giai đoạn | Hook | Việc |
|---|---|---|
| Enqueue | OnCreating |
Đọc WorkContext của luồng đang gọi, làm phẳng thành tham số wc:*. Không có WorkContext thì không ghi gì |
| Thực thi | OnPerforming |
Dựng lại WorkContext từ tham số; không có tham số nào thì dựng ngữ cảnh hệ thống |
| Xong | OnPerformed |
WorkContext = null, dispose Activity |
Theo loại job:
| Loại | Ngữ cảnh khi chạy |
|---|---|
| Fire-and-forget, Delayed, Continuation | Bản sao ngữ cảnh của luồng đã enqueue |
| Recurring đăng ký lúc khởi động | Ngữ cảnh hệ thống (system-scheduler / hangfire-recurring) |
| Enqueue từ một job khác | Ngữ cảnh của job cha, vì lúc đó WorkContext đang được nạp |
Ngữ cảnh bị ĐÓNG BĂNG lúc enqueue. Một job nằm trong hàng đợi qua đêm sẽ chạy với tên người
dùng, Roles, mã đối tác… đúng như thời điểm nó được tạo. Đó là hành vi đúng cho truy vết
(ai đã yêu cầu việc này), nhưng sai nếu dùng để phân quyền — quyền có thể đã bị thu hồi.
Job cần kiểm quyền thì phải đọc lại từ nguồn, đừng tin ngữ cảnh mang theo.
3. Hợp đồng trên dây: tham số wc:*
Hằng số ở JobParameterNames (public — gói cầu nối multi-tenancy đọc wc:siteCode).
Tiền tố wc: để không đụng tham số nội bộ của Hangfire. Trường rỗng không được ghi.
| Nhóm | Tham số |
|---|---|
| Trace | wc:correlationId, wc:traceParent |
| Tenant | wc:tenantId, wc:siteCode |
| User | wc:userId, wc:username, wc:fullName, wc:roles, wc:userIdentifier, wc:isMasterAccount |
| Client | wc:caller, wc:clientId, wc:language |
| Business | wc:agentCode, wc:linerCode, wc:partnerCode, wc:partnerType, wc:taxCode |
Không đi qua ranh giới job (có chủ đích hoặc đã biết):
| Trường | Vì sao |
|---|---|
Client.UserAgent, Client.Ip |
Thuộc về một request HTTP, không thuộc về job |
Tenant.IsolationLevel |
Đã gỡ khỏi ITenantContext (14/08/2026) — mức cách ly là sự thật hạ tầng, worker tra Tenant Store bằng wc:siteCode |
User.SubAccountIds |
⚠️ Khoảng trống đã biết, chưa vá. Job nhận danh sách rỗng. Không hại nếu nghiệp vụ chưa dùng tới; nhưng nếu phân quyền dựa vào sub-account thì job sẽ nhìn thấy phạm vi hẹp hơn thực tế |
4. CorrelationId đổi mỗi lần chạy — có chủ đích
OnPerforming luôn sinh CorrelationId mới, không dùng lại cái đã enqueue. Một job retry 5 lần
cho ra 5 CorrelationId khác nhau — nhờ vậy log của từng lần chạy tách bạch được.
Sợi dây nối về nguồn nằm ở Client.Caller:
Caller = "hangfire | <correlation-id-lúc-enqueue>" ← có ngữ cảnh khi enqueue
Caller = "hangfire-worker" ← không có
Đừng tra cứu xuyên ranh giới job bằng
CorrelationId— nó không còn giống nhau. DùngCaller, hoặctrace.id(§5), vốn thực sự xuyên suốt.
5. Nối lại trace
OnCreating ghi traceparent (W3C) của Activity đang chạy; OnPerforming dựng một Activity con
từ đó và dispose ở OnPerformed. Kết quả: trace.id trong log của job trùng với request đã
enqueue nó.
Không có traceparent (recurring job) thì vẫn khởi một Activity mới — để bản ghi trong lúc chạy có
trace.id riêng, thay vì rỗng.
6. Scope DI và cách ly giữa các job
Gói này không tạo scope nào. Khác TCIS.EventBus, nơi SubscribeInvoker tự mở một scope cho mỗi
message (await using var scope = _serviceProvider.CreateAsyncScope()), ở Hangfire scope do
JobActivator của host tạo — và bộ lọc chạy ngoài nó.
Thứ tự thật, đo trên Hangfire 1.8.18 (ghim bằng test Run_Server_Filters_Outside_The_Job_Scope):
OnPerforming (order tăng dần) → scope.begin → thân job → scope.dispose → OnPerformed (ngược lại)
Ba hệ quả:
| Bộ lọc không dùng được service Scoped | Lúc nó chạy chưa có scope nào. Đây là lý do TenantJobFilter nhận root provider |
| Ngữ cảnh được ghi trước khi scope tồn tại | Nên mọi service dựng trong scope (DbContext chọn chuỗi kết nối theo tenant) đều đọc được — cùng ý đồ với "chuẩn bị ngữ cảnh trước GetInstance" của EventBus |
| Dọn ngữ cảnh chạy sau khi scope đã dispose | Tenant vẫn còn nguyên trong lúc DbContext đóng kết nối. Ở điểm này Hangfire an toàn hơn EventBus, nơi Cleanup() chạy trước lúc scope được dispose |
Cách ly giữa hai job dựa vào OnPerformed, không dựa vào scope. Worker Hangfire là luồng sống
lâu, xử lý job này tới job khác. Ngữ cảnh nằm trong AsyncLocal, và thứ duy nhất xoá nó là
OnPerformed của bộ lọc. Không đăng ký bộ lọc thì không có gì dọn — job sau sẽ chạy với ngữ
cảnh của job trước.
EventBus không có rủi ro này vì scope của nó là ranh giới. Ở đây ranh giới là bộ lọc.
Ghim bằng test Not_Leak_The_Tenant_From_One_Job_To_The_Next: hai job chạy nối tiếp trên cùng một
luồng, job thứ hai không mang mã cảng và phải thấy trống.
Scope có được tạo hay không là việc của host, không phải của gói này. TCIS.Hangfire chỉ tham
chiếu Hangfire.Core, mà Hangfire.Core dùng JobActivator mặc định — nó gọi
Activator.CreateInstance, không có DI và không có scope. Muốn có scope một-job-một-scope
thì host phải dùng AddHangfireServer() của Hangfire.NetCore/Hangfire.AspNetCore, vốn cài
AspNetCoreJobActivator.
7. Job đa cảng
UseTWorkContext() một mình là KHÔNG đủ cho tầng dữ liệu multi-tenant. Nó dựng lại ngữ cảnh
nghiệp vụ (SiteCode, TenantId, user) nhưng không dựng hồ sơ tenant — chuỗi kết nối và mức
cách ly. Hai thứ đó nằm trong TenantInfo, mà TenantConnectionSource đọc từ
IMultiTenantContextAccessor; worker chạy trên thread nền và không bao giờ đi qua
MultiTenantMiddleware.
Triệu chứng: job chạm database ném "connection string is missing for tenant 'Unknown'". Job
không mở kết nối thì tệ hơn — TenantFilterPolicy fail-closed, truy vấn trả rỗng, và job
báo thành công sau khi không làm gì cả.
Thêm gói cầu nối, không phải sửa dòng nào trong job:
dotnet add package TCIS.Hangfire.MultiTenancy
builder.Services.AddTMultiTenant<TenantInfo>().WithConfigurationStore();
builder.Services.AddHangfire((sp, config) =>
{
config.UseSqlServerStorage(connectionString);
config.UseTWorkContext(); // ngữ cảnh nghiệp vụ
config.UseTMultiTenantJobContext(sp); // hồ sơ tenant
});
Thứ tự thực thi là điều kiện đúng đắn, không phải thẩm mỹ: OnPerforming của gói này gán toàn
bộ WorkContext mới, nên bộ lọc tenant chạy trước sẽ bị xoá sạch phần vừa nạp — hỏng im lặng, chỉ
lộ ra khi job đầu tiên chạm database. Bảo đảm bằng Order: bộ lọc tenant đăng ký 100, còn
UseTWorkContext() nhận DefaultOrder = -1 của Hangfire. Gõ hai dòng theo thứ tự nào cũng đúng.
Ứng dụng đơn tenant đơn giản là không gọi dòng thứ hai, và không có gì đổi.
Gọi UseTMultiTenantJobContext() mà quên UseTWorkContext() sẽ bị chặn, với mã
CONFIG_HANGFIRE_WORKCONTEXT_FILTER_MISSING. Thiếu bộ lọc nền thì không job nào mang
wc:siteCode, nên mọi job bị coi là job hệ thống và chạy không có cách ly — im lặng, vì
RequireTenant mặc định false. Đó là tổ hợp hỏng kín nhất của cặp gói này.
Job phải quét tất cả cảng (bộ lọc không giúp được, vì job chỉ mang một mã cảng hoặc không mang
mã nào) — xem md/14 §5 Cách 2.
8. Những chỗ dễ vấp
| # | Chỗ vấp | Hậu quả |
|---|---|---|
| 1 | Tra cứu xuyên ranh giới job bằng CorrelationId |
Không khớp — nó được sinh mới mỗi lần chạy (§4) |
| 2 | Dùng ngữ cảnh mang theo để phân quyền | Nó đóng băng từ lúc enqueue; quyền có thể đã bị thu hồi (§2) |
| 3 | Gọi UseTWorkContext() nhiều lần |
GlobalJobFilters không khử trùng lặp — bộ lọc chạy hai lần, và Activity của lần đầu bị ghi đè nên không được dispose |
| 4 | Chờ job đa cảng chạy đúng chỉ với UseTWorkContext() |
Truy vấn trả rỗng mà job vẫn báo thành công (§7) |
| 5 | Trông đợi SubAccountIds có mặt trong job |
Nó không được truyền — job nhận danh sách rỗng (§3) |
| 6 | Trông đợi bộ lọc dùng được service Scoped | Bộ lọc chạy ngoài scope của job; lúc đó chưa có scope nào (§6) |
| 7 | Bỏ UseTWorkContext() vì "job không cần WorkContext" |
Không còn gì dọn AsyncLocal — ngữ cảnh job trước dính sang job sau (§6). Nếu có dùng gói multi-tenancy thì job bị chặn với CONFIG_HANGFIRE_WORKCONTEXT_FILTER_MISSING (§7) |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Hangfire.Core (>= 1.8.18)
- TCIS.Core (>= 1.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on TCIS.Hangfire:
| Package | Downloads |
|---|---|
|
TCIS.Hangfire.MultiTenancy
TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. Multi-tenant context activation for TCIS Hangfire jobs. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 48 | 10/1/2026 |
| 1.0.0-rc.52 | 42 | 9/29/2026 |
| 1.0.0-rc.51 | 59 | 9/23/2026 |
| 1.0.0-rc.50 | 66 | 9/21/2026 |
| 1.0.0-rc.49 | 62 | 9/18/2026 |
| 1.0.0-rc.47 | 64 | 9/17/2026 |
| 1.0.0-rc.46 | 62 | 9/17/2026 |
| 1.0.0-rc.45 | 69 | 9/11/2026 |
| 1.0.0-rc.44 | 74 | 9/10/2026 |
| 1.0.0-rc.42 | 75 | 9/9/2026 |
| 1.0.0-rc.41 | 65 | 9/9/2026 |
| 1.0.0-rc.40 | 78 | 9/8/2026 |
| 1.0.0-rc.39 | 71 | 9/3/2026 |
| 1.0.0-rc.38 | 67 | 9/3/2026 |
| 1.0.0-rc.37 | 73 | 8/27/2026 |
| 1.0.0-rc.36 | 81 | 8/27/2026 |
| 1.0.0-rc.35 | 65 | 8/26/2026 |
| 1.0.0-rc.34 | 68 | 8/26/2026 |
| 1.0.0-rc.33 | 79 | 8/21/2026 |
| 1.0.0-rc.32 | 71 | 8/21/2026 |