Skip to main content

7.12 — Mini case study

Summary

Sự cố nghiêm trọng nhất có thể xảy ra trong hệ multi-tenant: người dùng của tenant A nhìn thấy dữ liệu của tenant B. Không có bug logic nào, không ai viết sai truy vấn — nguyên nhân là một lỗi cấu hình DI gọi là captive dependency: một service Singleton nhận vào một service Scoped, khiến service scoped đó bị "giam" và sống mãi với dữ liệu của request đầu tiên từng tạo ra nó. Điều khiến sự cố này đặc biệt nguy hiểm: nó không xuất hiện ở môi trường phát triển (chỉ một người dùng, một tenant) và không ném lỗi — hệ thống chạy hoàn toàn bình thường, chỉ trả sai dữ liệu.

Mục tiêu bài học​

Sau bài này bạn có thể:

  • Nhận ra captive dependency trong cấu hình DI.
  • Giải thích vì sao nó gây rò rỉ dữ liệu giữa tenant.
  • Sửa bằng IServiceScopeFactory.
  • Bật kiểm tra tự động để không tái diễn.

Nội dung bài học​

7.12.1 — Sự cố​

09:14  Khách hàng tenant "acme" báo: thấy 3 lead của công ty khác
trong danh sách của mình
09:20 Kiểm tra database: dữ liệu ĐÚNG, mọi lead đều có TenantId đúng
09:35 Chạy lại truy vấn bằng tay: trả về ĐÚNG
09:50 Reproduce trên staging: KHÔNG tái hiện được
10:30 Tái hiện được khi có HAI người dùng khác tenant cùng truy cập

Bốn đặc điểm khiến nó khó chẩn đoán:

  • Dữ liệu trong database đúng.
  • Truy vấn chạy tay đúng.
  • Môi trường phát triển không tái hiện (chỉ một người dùng).
  • Không có lỗi nào trong log.

7.12.2 — Nguyên nhân​

// Scoped — một instance MỚI cho mỗi request HTTP
public class TenantContext : ITenantContext
{
public TenantContext(IHttpContextAccessor accessor)
{
TenantId = accessor.HttpContext?.User.FindFirst("tenant_id")?.Value;
}

public string? TenantId { get; }
}

// SINGLETON — một instance duy nhất cho CẢ ỨNG DỤNG
public class CacheService : ICacheService
{
private readonly ITenantContext _tenantContext; // <- SCOPED trong SINGLETON

public CacheService(ITenantContext tenantContext) // BUG o day
{
_tenantContext = tenantContext;
}

public string BuildKey(string key) => $"{_tenantContext.TenantId}:{key}";
}
builder.Services.AddScoped<ITenantContext, TenantContext>();
builder.Services.AddSingleton<ICacheService, CacheService>(); // <- SAI
Request #1 (tenant "acme"):
-> DI tạo CacheService (singleton, tạo MỘT LẦN)
-> Để tạo nó, DI tạo TenantContext với TenantId = "acme"
-> CacheService GIỮ tham chiếu tới TenantContext đó MÃI MÃI

Request #2 (tenant "globex"):
-> CacheService đã tồn tại, KHÔNG tạo lại
-> Vẫn dùng TenantContext cũ -> TenantId = "acme"
-> BuildKey trả về "acme:leads" cho người dùng của globex
-> Đọc trúng cache của tenant khác -> RÒ RỈ DỮ LIỆU

Service scoped bị giam trong singleton — đó là tên gọi captive dependency.

Vì sao môi trường phát triển không phát hiện: chỉ có một người dùng, một tenant, nên TenantId bị giam tình cờ đúng với mọi request. Sự cố chỉ xuất hiện khi có tenant thứ hai.

7.12.3 — Vì sao DI container không báo lỗi​

Mặc định, .NET DI không kiểm tra vòng đời khi khởi động. Nó chỉ phát hiện lúc chạy và chỉ trong một số trường hợp:

Singleton  <- Singleton   : OK
Singleton <- Scoped : KHÔNG báo lỗi mặc định — đây là BUG
Singleton <- Transient : KHÔNG báo lỗi, nhưng transient bị giam
Scoped <- Singleton : OK
Scoped <- Transient : OK, transient sống bằng scope
Transient <- Scoped : OK trong scope

Hàng thứ hai là ca này. Hàng thứ ba cũng đáng lưu ý: Transient trong Singleton không được tạo mới mỗi lần như tên gọi gợi ý — nó cũng bị giam (bài 7.5).

7.12.4 — Sửa​

Cách 1 — dùng IServiceScopeFactory (đúng cho hầu hết trường hợp):

public class CacheService(IServiceScopeFactory scopeFactory) : ICacheService
{
public string BuildKey(string key)
{
using var scope = scopeFactory.CreateScope();
var tenantContext = scope.ServiceProvider.GetRequiredService<ITenantContext>();

return $"{tenantContext.TenantId}:{key}";
}
}

Mỗi lần gọi tạo một scope mới, lấy TenantContext của đúng request hiện tại, rồi huỷ scope. Không giữ gì cả.

Cách 2 — truyền tham số thay vì tiêm:

public class CacheService : ICacheService
{
// Không tiêm gì cả — người gọi truyền vào
public string BuildKey(string tenantId, string key) => $"{tenantId}:{key}";
}

Đơn giản nhất và thường đúng nhất: nếu một giá trị thay đổi theo request, nó là tham số, không phải dependency. Đây là cách sửa ít gây bất ngờ nhất.

Cách 3 — đổi vòng đời của service:

builder.Services.AddScoped<ICacheService, CacheService>();     // thay vì Singleton

Chỉ đúng nếu service thật sự không cần là singleton. Nếu nó giữ trạng thái dùng chung (một ConcurrentDictionary làm cache trong bộ nhớ chẳng hạn), đổi sang scoped sẽ tạo cache mới mỗi request và mất hết tác dụng.

7.12.5 — Bật kiểm tra tự động​

// Program.cs
builder.Host.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = true; // phát hiện captive dependency
options.ValidateOnBuild = true; // kiểm tra NGAY khi khởi động
});
System.AggregateException: Some services are not able to be constructed
---> InvalidOperationException: Cannot consume scoped service
'ITenantContext' from singleton 'ICacheService'.

Ứng dụng không khởi động được thay vì chạy sai. Đây là kết quả bạn muốn.

Hai tuỳ chọn khác nhau:

Tuỳ chọnKhi nào phát hiện
ValidateScopesLúc giải service — tức lúc chạy
ValidateOnBuildLúc khởi động — sớm nhất có thể

Bật cả hai. Trong môi trường phát triển, .NET bật ValidateScopes mặc định — nên sự cố này thường chỉ xuất hiện trên production, nơi nó bị tắt vì lý do hiệu năng. Hãy bật cả trên production: chi phí chỉ có ở lúc khởi động, và nó ngăn được đúng lớp sự cố như ca này.

Thêm một test chạy trong CI:

[Fact]
public void MoiService_DeuGiaiDuoc()
{
var factory = new WebApplicationFactory<Program>();
var provider = factory.Services;

using var scope = provider.CreateScope();

foreach (var descriptor in GetAllServiceDescriptors())
{
var service = scope.ServiceProvider.GetService(descriptor.ServiceType);
Assert.NotNull(service);
}
}

7.12.6 — Rà dự án​

# Tim moi dang ky singleton
grep -rn "AddSingleton" --include=*.cs src/

Với mỗi kết quả, kiểm tra constructor của lớp đó: mọi dependency của nó phải là singleton, hoặc phải là IServiceScopeFactory.

Ba dependency thường xuyên bị tiêm nhầm vào singleton:

DependencyVòng đờiHậu quả khi bị giam
DbContextScopedChange tracker phình vô hạn, dữ liệu cũ
IHttpContextAccessor dùng để lấy dữ liệuSingleton nhưng nội dung theo requestLấy sai người dùng
Bất kỳ ITenantContext, ICurrentUser nàoScopedRò rỉ dữ liệu giữa tenant

IHttpContextAccessor là ca tinh vi: bản thân nó là singleton nên DI không báo lỗi, nhưng nếu bạn đọc HttpContext trong constructor của một singleton, bạn chụp lại context của request đầu tiên. Đọc nó trong phương thức thì đúng.

// SAI — chụp context trong constructor của singleton
public class MyService(IHttpContextAccessor accessor)
{
private readonly string? _userId = accessor.HttpContext?.User...; // chụp MỘT LẦN
}

// ĐÚNG — đọc trong phương thức
public class MyService(IHttpContextAccessor accessor)
{
public string? GetUserId() => accessor.HttpContext?.User...; // đọc MỖI LẦN
}

7.12.7 — Rà lại code của bạn​

Danh sách rà soát captive dependency

  • •ValidateScopes và ValidateOnBuild được bật ở mọi môi trường.
  • •Mọi singleton chỉ nhận dependency singleton hoặc IServiceScopeFactory.
  • •Không tiêm DbContext vào singleton.
  • •Không đọc HttpContext trong constructor của singleton.
  • •Giá trị thay đổi theo request được truyền làm tham số, không tiêm.
  • •Có test trong CI giải mọi service đã đăng ký.
  • •Đã rà toàn bộ AddSingleton và kiểm tra dependency của chúng.

Bài tập áp dụng​

Bài 1 — Tái hiện sự cố​

Tạo một singleton nhận một scoped, gọi endpoint với hai giá trị header khác nhau, và xác nhận giá trị thứ hai vẫn trả về của lần đầu.

Tiêu chí hoàn thành: bạn thấy singleton giữ mãi một giá trị trong khi mỗi scope có giá trị riêng, và hiểu vì sao lỗi này không ném ngoại lệ nào.

Gợi ý và lời giải — Bài 1

Gợi ý. Tắt ValidateScopes để thấy hành vi thật. Với kiểm tra bật, ứng dụng không khởi động — và bạn sẽ không thấy được hậu quả.

Lời giải:

public class NguCanhYeuCau { public int TenantId { get; set; } }              // Scoped
public class DichVuNen(NguCanhYeuCau nc) { public int DocTenant() => nc.TenantId; } // Singleton
builder.Services.AddScoped<NguCanhYeuCau>();
builder.Services.AddSingleton<DichVuNen>();

builder.Host.UseDefaultServiceProvider((_, o) =>
{
o.ValidateScopes = false; // tắt để THẤY hậu quả
o.ValidateOnBuild = false;
});
for (int i = 1; i <= 3; i++)
{
using var scope = app.Services.CreateScope();
scope.ServiceProvider.GetRequiredService<NguCanhYeuCau>().TenantId = i;

var dv = app.Services.GetRequiredService<DichVuNen>();
Console.WriteLine($"yêu cầu {i}: scoped mới có TenantId={i}, " +
$"singleton đọc được TenantId={dv.DocTenant()}");
}

Kết quả chạy trên .NET 9.0.203:

  yêu cầu 1: scoped mới có TenantId=1, singleton đọc được TenantId=0
yêu cầu 2: scoped mới có TenantId=2, singleton đọc được TenantId=0
yêu cầu 3: scoped mới có TenantId=3, singleton đọc được TenantId=0

Singleton đọc TenantId=0 ở cả ba lần — không bao giờ thấy giá trị nào của các scope.

Vì sao:

Singleton được khởi tạo MỘT lần, lúc đầu tiên có người hỏi tới nó.
Lúc đó, container phải cung cấp một NguCanhYeuCau
-> nó tạo một instance và GIỮ luôn trong singleton

Mọi scope sau đó tạo instance MỚI của NguCanhYeuCau.
Singleton không biết gì về chúng — nó vẫn cầm instance đầu tiên.
Instance đầu tiên đó "bị giam" trong singleton -> tên gọi captive dependency.

Và vì sao không có ngoại lệ nào:

Về mặt kỹ thuật, không có gì sai:
- NguCanhYeuCau được tạo thành công
- DichVuNen giữ một tham chiếu hợp lệ
- mọi lời gọi đều chạy trơn tru

Container không có cách nào biết rằng BẠN muốn nó lấy instance của scope hiện tại.
Hậu quả trong một hệ nhiều tenant:
request của tenant B chạy qua singleton -> đọc được TenantId của tenant A
-> truy vấn dữ liệu của A -> trả về cho B

Đây không phải lỗi hiệu năng hay lỗi ổn định. Đây là RÒ RỈ DỮ LIỆU.

Ba dấu hiệu nhận ra trên hệ thống đang chạy:

1. "Thỉnh thoảng người dùng thấy dữ liệu của người khác"
2. Lỗi biến mất sau khi khởi động lại, rồi quay lại sau vài giờ
3. Chỉ xảy ra khi có nhiều người dùng đồng thời — không tái hiện được một mình

Dấu hiệu thứ hai đáng chú ý: sau khi khởi động lại, singleton được tạo lại trong ngữ cảnh của request đầu tiên, nên nó "đúng" cho đúng tenant đó — và sai cho mọi tenant còn lại.

Cách sửa, theo thứ tự nên cân nhắc:

// 1. Service này có thật sự cần Singleton không?
builder.Services.AddScoped<DichVuNen>();

// 2. Nếu buộc phải Singleton: nhận factory, tạo scope khi cần
public class DichVuNen(IServiceScopeFactory factory)
{
public async Task LamViecAsync(CancellationToken ct)
{
using var scope = factory.CreateScope();
var nc = scope.ServiceProvider.GetRequiredService<NguCanhYeuCau>();
await XuLyAsync(nc.TenantId, ct);
}
}

// 3. Hoặc: truyền dữ liệu vào tham số, không tiêm ngữ cảnh
public async Task LamViecAsync(int tenantId, CancellationToken ct) { }

Cách thứ ba thường là cách sạch nhất: singleton không cần biết về "ngữ cảnh yêu cầu" chút nào — người gọi biết, và truyền xuống.

Và một biến thể rất phổ biến của cùng lỗi này:

// IHttpContextAccessor trong một Singleton
public class DichVuNen(IHttpContextAccessor accessor)
{
public string? LayNguoiDung() => accessor.HttpContext?.User.Identity?.Name;
}
IHttpContextAccessor được đăng ký Singleton và dùng AsyncLocal bên trong,
nên trường hợp này KHÔNG bị captive — nó trả về context đúng của luồng hiện tại.

NHƯNG: gọi nó từ một BackgroundService -> HttpContext là null
-> NullReferenceException, hoặc tệ hơn là bỏ qua âm thầm với toán tử ?.

Bài 2 — Bật kiểm tra​

Thêm ValidateOnBuild và xác nhận ứng dụng không khởi động được.

Tiêu chí hoàn thành: bạn có thông điệp lỗi chính xác, và biết ValidateScopes với ValidateOnBuild bắt được hai thứ khác nhau.

Gợi ý và lời giải — Bài 2

Gợi ý. Bật riêng từng cái và xem cái nào bắt được lỗi ở bài 1.

Lời giải:

builder.Host.UseDefaultServiceProvider((_, o) =>
{
o.ValidateScopes = true;
o.ValidateOnBuild = true;
});

var app = builder.Build(); // ném lỗi NGAY tại dòng này

Kết quả chạy trên .NET 9.0.203:

AggregateException: Some services are not able to be constructed
(Error while validating the service descriptor
'ServiceType: DichVuNen Lifetime: Singleton ImplementationType: DichVuNen':
Cannot consume scoped service 'NguCanhYeuCau' from singleton 'DichVuNen'.)

Thông điệp nói đủ ba thứ cần biết: lớp nào, vòng đời nào, và phụ thuộc nào.

Hai tuỳ chọn bắt được hai thứ khác nhau:

ValidateScopesValidateOnBuild
Khi nào chạyMỗi lần phân giải một serviceMột lần, lúc Build()
Bắt đượcScoped bị dùng từ Singleton hoặc từ gốcToàn bộ đồ thị: thiếu đăng ký, vòng đời sai
Chi phíNhỏ, ở mỗi lần phân giảiVài chục ms, một lần
Mặc địnhBật ở DevelopmentTắt ở mọi môi trường
ValidateOnBuild duyệt MỌI service đã đăng ký và thử dựng đồ thị phụ thuộc
-> bắt được cả lỗi "quên đăng ký IKhoLead" mà ValidateScopes không thấy
(vì service đó chưa bao giờ được phân giải trong lúc chạy)

Ví dụ lỗi thứ hai mà chỉ ValidateOnBuild bắt được:

builder.Services.AddScoped<LeadService>();       // LeadService cần IKhoLead
// quên: builder.Services.AddScoped<IKhoLead, KhoLead>();
Không có ValidateOnBuild:
ứng dụng khởi động bình thường
-> request đầu tiên vào endpoint dùng LeadService mới ném lỗi
-> nếu endpoint đó ít dùng, có thể vài ngày sau

Có ValidateOnBuild:
Unable to resolve service for type 'IKhoLead' while attempting to
activate 'LeadService'.
-> ngay lúc khởi động

Vì sao mặc định tắt, và vì sao vẫn nên bật:

Lý do mặc định tắt: chi phí khởi động, và một số mẫu đăng ký hợp lệ
(ví dụ factory tạo service theo điều kiện lúc chạy)
có thể báo lỗi giả

Nhưng: vài chục mili giây một lần, đổi lấy việc loại bỏ hẳn hai lớp lỗi
-> gần như luôn đáng, kể cả trên production
// Nếu có một vài đăng ký gây báo lỗi giả, loại trừ riêng chúng
// thay vì tắt kiểm tra cho toàn bộ ứng dụng

Và một kiểm chứng nên có trong bộ test — nó bắt lỗi sớm hơn cả khởi động:

[Fact]
public void Moi_service_dang_ky_deu_dung_duoc()
{
using var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(b => b.UseDefaultServiceProvider(o =>
{
o.ValidateScopes = true;
o.ValidateOnBuild = true;
}));

// Chỉ cần dựng host là đủ — ValidateOnBuild làm phần còn lại
var _ = factory.Services;
}
[Fact]
public void Moi_endpoint_phan_giai_duoc_phu_thuoc()
{
using var scope = _factory.Services.CreateScope();

foreach (var t in typeof(Program).Assembly.GetTypes()
.Where(t => t.Name.EndsWith("Controller") || t.Name.EndsWith("Handler")))
{
var act = () => ActivatorUtilities.CreateInstance(scope.ServiceProvider, t);
act.Should().NotThrow($"{t.Name} phải dựng được từ container");
}
}
Test thứ hai đi xa hơn ValidateOnBuild: nó thử DỰNG THẬT từng controller
và handler, nên bắt được cả những lỗi chỉ lộ ra khi tạo instance.

Bài 3 — Rà dự án​

Liệt kê mọi AddSingleton và kiểm tra constructor của từng lớp.

Tiêu chí hoàn thành: bạn có danh sách kèm đánh giá, và phân biệt được singleton đúng với singleton do quán tính.

Gợi ý và lời giải — Bài 3

Gợi ý. Với mỗi singleton, đọc constructor của nó. Nếu có một tham số là DbContext hay repository, dừng lại xem kỹ.

Lời giải — liệt kê:

grep -rn "AddSingleton" --include="*.cs" src/ | grep -v "/Tests/"
src/Crm.Api/Program.cs:34:  builder.Services.AddSingleton<IBoDemLuot, BoDemLuot>();
src/Crm.Api/Program.cs:35: builder.Services.AddSingleton<ICacheCauHinh, CacheCauHinh>();
src/Crm.Api/Program.cs:36: builder.Services.AddSingleton<IMaHoa, MaHoaAes>();
src/Crm.Api/Program.cs:37: builder.Services.AddSingleton<IDongBoService, DongBoService>();
src/Crm.Api/Program.cs:38: builder.Services.AddSingleton<IThongKeService, ThongKeService>();
src/Crm.Api/Program.cs:39: builder.Services.AddSingleton(TimeProvider.System);

Rồi đọc constructor của từng lớp:

for c in BoDemLuot CacheCauHinh MaHoaAes DongBoService ThongKeService; do
echo "--- $c"
grep -A4 -E "public $c\(" $(grep -rl "class $c" --include="*.cs" src/) | head -6
done

Bảng đánh giá:

LớpPhụ thuộcĐánh giá
BoDemLuotkhông cóĐúng — không trạng thái theo request
CacheCauHinhIOptionsMonitor<T>Đúng — Monitor là Singleton
MaHoaAesIOptions<T>Đúng
DongBoServiceCrmDbContextSAI — captive
ThongKeServiceIKhoLead (Scoped)SAI — captive
TimeProvider.System—Đúng

Hai lớp sai — và cả hai đều sai theo cùng một cách:

// SAI
public class DongBoService(CrmDbContext db) : IDongBoService
{
public async Task DongBoAsync(CancellationToken ct) { /* dùng db */ }
}
CrmDbContext đăng ký Scoped (mặc định của AddDbContext).
Singleton giữ một instance mãi mãi
-> change tracker phình to không giới hạn
-> kết nối bị giữ
-> và dữ liệu đọc được là ảnh chụp từ lúc nào đó trong quá khứ
// ĐÚNG — tạo scope cho mỗi lượt làm việc
public class DongBoService(IServiceScopeFactory factory) : IDongBoService
{
public async Task DongBoAsync(CancellationToken ct)
{
using var scope = factory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<CrmDbContext>();
await XuLyAsync(db, ct);
}
}

Và đây là phần đáng suy nghĩ hơn: vì sao hai lớp này được đăng ký Singleton?

Thường không phải vì có lý do. Ba nguyên nhân hay gặp:

1. "Singleton nghe có vẻ hiệu quả hơn"
-> nhưng Scoped chỉ tạo một instance mỗi request, chi phí không đáng kể

2. Sao chép từ một dòng AddSingleton ở ngay trên
-> và dòng đó đúng, vì lớp kia không có phụ thuộc scoped

3. Cần giữ trạng thái giữa các request
-> đây là lý do THẬT, nhưng cách giải quyết không phải Singleton
giữ DbContext, mà là Singleton giữ DỮ LIỆU và tự tạo scope khi cần

Phân biệt singleton đúng với singleton do quán tính — ba câu hỏi:

1. Lớp này có TRẠNG THÁI cần chia sẻ giữa các request không?
Không -> Scoped là mặc định an toàn hơn

2. Việc khởi tạo có đắt không? (nạp file, dựng bảng tra cứu, mở kết nối lâu dài)
Không -> không có lợi ích gì từ Singleton

3. Constructor của nó có phụ thuộc nào là Scoped không?
Có -> hoặc đổi sang Scoped, hoặc dùng IServiceScopeFactory
Nếu cả ba câu đều "không có lý do" -> đăng ký Scoped.
Scoped là mặc định đúng cho phần lớn service trong ASP.NET Core.

Ba trường hợp Singleton là lựa chọn đúng rõ ràng:

// 1. Không trạng thái, và khởi tạo đắt
builder.Services.AddSingleton<IMaHoa, MaHoaAes>(); // dựng khoá một lần

// 2. Giữ trạng thái CỐ TÌNH chia sẻ
builder.Services.AddSingleton<IBoDemLuot, BoDemLuot>(); // bộ đếm toàn ứng dụng

// 3. Bọc một tài nguyên vốn đã dùng chung
builder.Services.AddSingleton<IConnectionMultiplexer>(
_ => ConnectionMultiplexer.Connect(redisCs)); // Redis khuyến nghị dùng chung

Và cách chặn lỗi này quay lại — một test đọc chính bảng đăng ký:

[Fact]
public void Singleton_khong_duoc_phu_thuoc_vao_scoped()
{
var services = new ServiceCollection();
Program.DangKyDichVu(services, _cauHinh); // tách phần đăng ký ra để test được

var scoped = services.Where(d => d.Lifetime == ServiceLifetime.Scoped)
.Select(d => d.ServiceType).ToHashSet();

var viPham = services
.Where(d => d.Lifetime == ServiceLifetime.Singleton && d.ImplementationType is not null)
.SelectMany(d => d.ImplementationType!.GetConstructors()
.SelectMany(c => c.GetParameters())
.Where(p => scoped.Contains(p.ParameterType))
.Select(p => $"{d.ImplementationType!.Name} nhận {p.ParameterType.Name}"))
.ToList();

viPham.Should().BeEmpty("singleton giữ scoped sẽ giam instance đầu tiên mãi mãi");
}
Test này làm đúng việc bạn vừa làm bằng tay, nhưng chạy tự động
với mỗi pull request — và nó không quên như con người.

Tự kiểm tra​

Frequently asked questions

Captive dependency là gì?

Là khi một service singleton nhận vào một service scoped hoặc transient. Service đó bị giam và sống mãi cùng singleton, giữ nguyên trạng thái của lần đầu tiên nó được tạo.

Vì sao nó gây rò rỉ dữ liệu giữa tenant?

Vì service scoped chứa thông tin tenant của request đầu tiên bị giữ mãi. Mọi request sau, kể cả của tenant khác, đều dùng lại giá trị đó, nên khoá cache và bộ lọc dữ liệu đều trỏ nhầm tenant.

Vì sao môi trường phát triển không phát hiện được?

Vì chỉ có một người dùng và một tenant, nên giá trị bị giam tình cờ đúng với mọi request. Sự cố chỉ xuất hiện khi có tenant thứ hai truy cập đồng thời.

Vì sao Transient trong Singleton cũng là vấn đề?

Vì nó không được tạo mới mỗi lần như tên gọi gợi ý. Nó được tạo một lần lúc dựng singleton rồi bị giữ mãi, nên mọi giả định về việc nó luôn mới đều sai.

Cách sửa nào ít gây bất ngờ nhất?

Truyền giá trị làm tham số thay vì tiêm. Nếu một giá trị thay đổi theo request thì nó là tham số chứ không phải dependency. Cách này không cần tạo scope và không có cạm bẫy nào.

IHttpContextAccessor trong singleton có an toàn không?

Bản thân nó là singleton nên DI không báo lỗi, nhưng đọc HttpContext trong constructor của singleton sẽ chụp lại context của request đầu tiên. Phải đọc nó trong phương thức, không phải trong constructor.

Kết luận​

Ba điều đáng nhớ nhất:

  1. Singleton chỉ được nhận singleton hoặc IServiceScopeFactory.
  2. Bật ValidateOnBuild ở mọi môi trường — ứng dụng không chạy còn hơn chạy sai.
  3. Giá trị thay đổi theo request là tham số, không phải dependency.

Tham khảo​

Điều hướng​