TCIS.Hangfire.MultiTenancy
1.0.0
See the version list below for details.
dotnet add package TCIS.Hangfire.MultiTenancy --version 1.0.0
NuGet\Install-Package TCIS.Hangfire.MultiTenancy -Version 1.0.0
<PackageReference Include="TCIS.Hangfire.MultiTenancy" Version="1.0.0" />
<PackageVersion Include="TCIS.Hangfire.MultiTenancy" Version="1.0.0" />
<PackageReference Include="TCIS.Hangfire.MultiTenancy" />
paket add TCIS.Hangfire.MultiTenancy --version 1.0.0
#r "nuget: TCIS.Hangfire.MultiTenancy, 1.0.0"
#:package TCIS.Hangfire.MultiTenancy@1.0.0
#addin nuget:?package=TCIS.Hangfire.MultiTenancy&version=1.0.0
#tool nuget:?package=TCIS.Hangfire.MultiTenancy&version=1.0.0
TCIS.Hangfire.MultiTenancy
Cho worker Hangfire tự nạp hồ sơ tenant từ tham số job wc:siteCode, để job chạy được trên tầng
dữ liệu đa cảng của TCIS.Pluggable.
Gói cầu nối: nó biết cả hai bên, để không bên nào phải biết bên kia. TCIS.Hangfire giữ nguyên
purity — chỉ tham chiếu TCIS.Core.
Mục lục
- Vấn đề nó giải
- Cài đặt
- Hành vi
- Bộ lọc chạy NGOÀI scope DI của job
- Job quét tất cả các cảng
- Những chỗ dễ vấp
1. Vấn đề nó giải
TWorkContextJobFilter khôi phục WorkContext — đủ để định tuyến nghiệp vụ theo cảng, nhưng
không đủ để mở kết nối.
Tầng dữ liệu đọc một nguồn khác: TenantConnectionSource và TenantFilterPolicy đều lấy TenantInfo
từ IMultiTenantContextAccessor — nơi chứa chuỗi kết nối và mức cách ly. Worker chạy trên
thread nền, không đi qua MultiTenantMiddleware, nên không thành phần nào nạp nó.
| Host đa cảng KHÔNG cài gói này | |
|---|---|
| Job chạm database | ném "connection string is missing for tenant 'Unknown'" |
| Job không mở kết nối | TenantFilterPolicy fail-closed ⇒ truy vấn trả rỗng, job báo thành công, không ai biết |
Vế thứ hai nguy hiểm hơn: hệ thống báo thành công sau khi không làm gì cả.
2. Cài đặt
dotnet add package TCIS.Hangfire.MultiTenancy
// 1. Tenant Store — nguồn sự thật.
// KHÔNG cần strategy: mã cảng đã có sẵn trong tham số job.
builder.Services.AddTMultiTenant<TenantInfo>()
.WithConfigurationStore();
// 2. Overload (sp, config) — bộ lọc cần IServiceProvider.
builder.Services.AddHangfire((sp, config) =>
{
config.UseSqlServerStorage(connectionString);
config.UseTWorkContext(); // ngữ cảnh nghiệp vụ ← BẮT BUỘC
config.UseTMultiTenantJobContext(sp); // hồ sơ tenant
});
builder.Services.AddHangfireServer();
Host đơn tenant không gọi dòng thứ hai, và không có gì đổi.
Quên UseTWorkContext() sẽ bị chặn với mã CONFIG_HANGFIRE_WORKCONTEXT_FILTER_MISSING.
Thiếu nó thì OnCreating không chạy, nên không job nào mang wc:siteCode. Bộ lọc tenant khi đó
coi mọi job là job hệ thống — và vì RequireTenant mặc định false, nó không kêu một tiếng
nào. Toàn bộ job đa cảng chạy không có cách ly. Đây là tổ hợp hỏng kín nhất của cặp gói này, nên
nó được chặn tường minh.
Thứ tự gõ hai dòng không quan trọng. Thứ tự thực thi mới quan trọng, và nó được bảo đảm bằng
Order: bộ lọc này đăng ký 100, UseTWorkContext() nhận DefaultOrder = -1 của Hangfire. Điều đó
cần thiết vì TWorkContextJobFilter.OnPerforming gán toàn bộ WorkContext mới — chạy nó sau thì
phần Tenant vừa nạp bị xoá sạch.
Gọi UseTMultiTenantJobContext nhiều lần là an toàn: lần sau thắng, không tạo bộ lọc trùng.
3. Hành vi
| Job | Kết quả |
|---|---|
Không có wc:siteCode |
Job hệ thống — chạy bình thường |
| Có mã cảng, Store biết | Nạp TenantInfo + phần Tenant của WorkContext |
| Có mã cảng, Store không biết | Luôn từ chối (SEC_TENANT_NOT_FOUND), bất kể RequireTenant |
Hồ sơ tenant khai thiếu IsolationLevel |
Từ chối (CONFIG_TENANT_ISOLATION_TIER_MISSING) |
// Worker CHỈ phục vụ job của cảng — chặn luôn job không mang mã cảng:
config.UseTMultiTenantJobContext(sp, o => o.RequireTenant = true);
RequireTenant mặc định false — ngược với TCIS.EventBus.MultiTenancy.
| Mặc định | Vì sao | |
|---|---|---|
| Hangfire | false |
Job hệ thống (recurring lúc khởi động, job dọn dẹp) chạy với TenantId = null — đó là chuyện bình thường |
| EventBus | true |
Mọi message đều xuất phát từ nghiệp vụ của một cảng; message không có mã cảng là bất thường |
Nhưng "có mã cảng mà Store không biết" thì LUÔN bị từ chối ở cả hai bên. Bất đối xứng đó có chủ đích: không có mã cảng là trạng thái hợp lệ; mã cảng sai là lỗi cấu hình.
AutomaticRetryAttribute của Hangfire vẫn áp dụng cho ngoại lệ ném từ bộ lọc. Một mã cảng sai
trong cấu hình sẽ bị thử lại theo chính sách retry của job — mà lỗi cấu hình thì không tự khỏi. Với
job phụ thuộc tenant, cân nhắc [AutomaticRetry(Attempts = 0)].
Đây là điểm EventBus hơn: bên đó IsPermanentFailure nhận ra tiền tố SEC_/CONFIG_ và không
thử lại. Hangfire giữ cơ chế retry ở ngoài tầm với của bộ lọc.
4. Bộ lọc chạy NGOÀI scope DI của job
Đ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)
| Hệ quả | |
|---|---|
| Bộ lọc không dùng được service Scoped | Lúc nó chạy chưa có scope. Vì thế nó nhận root provider — hợp lệ, vì ResolveTenantAsync/ApplyTenant chỉ cần Singleton + AsyncLocal |
| Ngữ cảnh ghi trước khi scope tồn tại | DbContext dựng trong scope đọc được chuỗi kết nối theo tenant |
| Dọn ngữ cảnh chạy sau khi scope dispose | Tenant còn nguyên lúc DbContext đóng kết nối — an toàn hơn EventBus, nơi Cleanup() chạy trước lúc scope dispose |
Cách ly giữa hai job dựa vào OnPerformed, KHÔNG dựa vào scope. Đây là khác biệt kiến trúc so
với EventBus, nơi SubscribeInvoker tự mở một scope mỗi message và scope chính là ranh giới.
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 — không đăng ký bộ lọc thì không có gì dọn.
5. Job quét tất cả các cảng
Bộ lọc không giúp được ở đây: một job chỉ mang một mã cảng, hoặc không mang mã nào. Dùng trực tiếp
cặp hàm của TCIS.MultiTenancy:
[AutomaticRetry(Attempts = 0)]
public async Task ExecuteAsync(CancellationToken ct)
{
foreach (TenantInfo tenant in await tenantStore.GetAllAsync())
{
TenantInfo? profile = await serviceProvider.ResolveTenantAsync(tenant.Identifier, ct);
// ĐỒNG BỘ. false ⇒ không phân giải được ⇒ bỏ qua cảng này, KHÔNG chạy với ngữ cảnh thiếu.
if (!serviceProvider.ApplyTenant(profile)) continue;
try { await billingProcessor.ProcessDailyInvoiceAsync(ct); }
catch { /* log cho riêng cảng này, chạy tiếp cảng khác */ }
finally { serviceProvider.ClearTenant(); }
}
}
ApplyTenant phải đồng bộ: ghi AsyncLocal sau một await đã nhường luồng thì thay đổi không lan
ra ngoài, và nó hỏng không tất định.
6. Những chỗ dễ vấp
| # | Chỗ vấp | Hậu quả |
|---|---|---|
| 1 | Cài TCIS.Pluggable nhưng quên gói này |
Truy vấn trả rỗng mà job vẫn báo thành công (§1) |
| 2 | Gọi UseTMultiTenantJobContext() mà quên UseTWorkContext() |
Bị chặn với CONFIG_HANGFIRE_WORKCONTEXT_FILTER_MISSING (§2) |
| 3 | Dùng overload AddHangfire(config => …) không có sp |
Không có IServiceProvider để truyền — đổi sang AddHangfire((sp, config) => …) |
| 4 | Trông đợi bộ lọc dùng được service Scoped | Nó chạy ngoài scope của job (§4) |
| 5 | Để AutomaticRetry mặc định cho job phụ thuộc tenant |
Lỗi cấu hình bị thử lại 10 lần mà không tự khỏi (§3) |
| 6 | Mong bộ lọc lo được job quét mọi cảng | Job chỉ mang một mã cảng — dùng mẫu ở §5 |
Chi tiết vận hành: md/14 §6. Bộ test đối chứng: TCIS.Hangfire.MultiTenancy.Tests (16 test, chạy
qua BackgroundJobPerformer thật, không cần Docker).
| 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)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Configuration (>= 9.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.0)
- Microsoft.Extensions.DependencyInjection (>= 9.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Http (>= 9.0.0)
- Microsoft.Extensions.Logging (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.0)
- TCIS.Hangfire (>= 1.0.0)
- TCIS.MultiTenancy (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.1-rc.1 | 0 | 10/5/2026 |
| 1.0.0 | 62 | 10/1/2026 |
| 1.0.0-rc.52 | 40 | 9/29/2026 |
| 1.0.0-rc.51 | 59 | 9/23/2026 |
| 1.0.0-rc.50 | 54 | 9/21/2026 |
| 1.0.0-rc.49 | 61 | 9/18/2026 |
| 1.0.0-rc.47 | 63 | 9/17/2026 |
| 1.0.0-rc.46 | 58 | 9/17/2026 |
| 1.0.0-rc.45 | 69 | 9/11/2026 |
| 1.0.0-rc.44 | 65 | 9/10/2026 |
| 1.0.0-rc.43 | 67 | 9/10/2026 |
| 1.0.0-rc.42 | 71 | 9/9/2026 |
| 1.0.0-rc.41 | 65 | 9/9/2026 |
| 1.0.0-rc.40 | 64 | 9/8/2026 |
| 1.0.0-rc.39 | 67 | 9/3/2026 |
| 1.0.0-rc.38 | 62 | 9/3/2026 |
| 1.0.0-rc.37 | 77 | 8/27/2026 |
| 1.0.0-rc.36 | 91 | 8/27/2026 |
| 1.0.0-rc.35 | 65 | 8/26/2026 |
| 1.0.0-rc.34 | 70 | 8/26/2026 |