7.13 — Ví dụ thực tế nhanh
Một tình huống gây bối rối rất hay gặp: đổi giá trị trong appsettings.json, khởi động lại ứng dụng, và giá trị vẫn là cũ. Hoặc ngược lại — đổi cấu hình trong lúc chạy và một nửa hệ thống nhận giá trị mới còn nửa kia vẫn dùng giá trị cũ. Nguyên nhân là .NET có ba interface Options trông gần giống nhau nhưng hành xử khác hẳn: IOptions, IOptionsSnapshot và IOptionsMonitor. Chọn sai gây lỗi im lặng theo ba kiểu khác nhau. Có một luật cứng đáng nhớ trước mọi thứ khác: singleton không dùng được IOptionsSnapshot — nó là scoped, nên tiêm vào singleton là captive dependency.
Mục tiêu bài học
Sau bài này bạn có thể:
- Phân biệt ba interface Options và chọn đúng cái.
- Biết cấu hình nào đọc lại được và cấu hình nào không.
- Xác thực cấu hình ngay lúc khởi động.
- Tránh lỗi tiêm
IOptionsSnapshotvào singleton.
Nội dung bài học
7.13.1 — Ba interface
public class EmailOptions
{
public string SmtpHost { get; set; } = "";
public int Port { get; set; }
public int TimeoutSeconds { get; set; }
}
builder.Services.Configure<EmailOptions>(builder.Configuration.GetSection("Email"));
| Interface | Vòng đời | Đọc lại khi file đổi | Dùng trong |
|---|---|---|---|
IOptions<T> | Singleton | Không bao giờ | Mọi nơi |
IOptionsSnapshot<T> | Scoped | Mỗi request | Không dùng trong singleton |
IOptionsMonitor<T> | Singleton | Ngay lập tức | Mọi nơi, kể cả singleton |
// IOptions — đọc MỘT LẦN lúc khởi động
public class EmailSender(IOptions<EmailOptions> options)
{
private readonly EmailOptions _config = options.Value; // cố định mãi mãi
}
// IOptionsSnapshot — đọc lại mỗi request
public class EmailSender(IOptionsSnapshot<EmailOptions> options)
{
public void Send() { var config = options.Value; } // mới cho request này
}
// IOptionsMonitor — luôn là giá trị mới nhất
public class EmailSender(IOptionsMonitor<EmailOptions> options)
{
public void Send() { var config = options.CurrentValue; } // đọc TẠI THỜI ĐIỂM gọi
}
Lưu ý IOptionsMonitor dùng CurrentValue, không phải Value — đây là chi tiết dễ nhầm.
7.13.2 — Ba lỗi tương ứng
Lỗi 1 — dùng IOptions rồi mong nó cập nhật:
public class RateLimitService(IOptions<RateLimitOptions> options)
{
private readonly RateLimitOptions _config = options.Value;
public bool Cho(string userId) => _count[userId] < _config.MaxPerMinute;
}
Đổi MaxPerMinute trong appsettings.json khi ứng dụng đang chạy → không có tác dụng gì. IOptions đọc một lần lúc khởi động.
Nếu muốn đổi cấu hình mà không khởi động lại, dùng IOptionsMonitor.
Lỗi 2 — tiêm IOptionsSnapshot vào singleton:
// LOI — IOptionsSnapshot la SCOPED
builder.Services.AddSingleton<ICacheService, CacheService>();
public class CacheService(IOptionsSnapshot<CacheOptions> options) : ICacheService
InvalidOperationException: Cannot consume scoped service
'IOptionsSnapshot<CacheOptions>' from singleton 'ICacheService'.
Đây chính là captive dependency (bài 7.12). Với singleton, dùng IOptions (nếu giá trị cố định) hoặc IOptionsMonitor (nếu cần cập nhật).
Lỗi 3 — mong IOptionsMonitor cập nhật từ nguồn không hỗ trợ:
// Biến môi trư ờng — KHÔNG đọc lại được
builder.Configuration.AddEnvironmentVariables();
// Azure Key Vault — chỉ đọc lại nếu bật reload interval
builder.Configuration.AddAzureKeyVault(uri, credential,
new AzureKeyVaultConfigurationOptions { ReloadInterval = TimeSpan.FromMinutes(5) });
| Nguồn cấu hình | Đọc lại được |
|---|---|
appsettings.json | Có (reloadOnChange: true, mặc định) |
| Biến môi trường | Không — đọc một lần lúc khởi động |
| Tham số dòng lệnh | Không |
| Azure Key Vault | Chỉ khi đặt ReloadInterval |
| User secrets | Có |
Đây là câu trả lời cho câu hỏi ở đầu bài: nếu bạn đổi cấu hình trong biến môi trường của container, IOptionsMonitor cũng không giúp gì — phải khởi động lại container. Và nếu bạn đổi trong appsettings.json bên trong image, file trên đĩa không đổi chút nào (bài 15.5).
7.13.3 — Chọn interface nào
Cấu hình có đổi lúc chạy không?
├─ Không -> IOptions<T> (đơn giản nhất)
└─ Có
├─ Dùng trong service scoped hoặc transient -> IOptionsSnapshot<T>
└─ Dùng trong singleton -> IOptionsMonitor<T>
Khuyến nghị thực dụng: dùng IOptions<T> làm mặc định. Phần lớn cấu hình — chuỗi kết nối, tên host, tên hàng đợi — không đổi trong lúc chạy, và khởi động lại khi đổi cấu hình là hành vi bình thường, dễ hiểu.
Chỉ dùng IOptionsMonitor cho những giá trị thật sự cần đổi nóng: mức ghi log, feature flag, ngưỡng rate limit.
7.13.4 — Phản ứng khi cấu hình đổi
public class EmailSender : IDisposable
{
private readonly IDisposable? _registration;
private EmailOptions _config;
public EmailSender(IOptionsMonitor<EmailOptions> monitor)
{
_config = monitor.CurrentValue;
_registration = monitor.OnChange(config =>
{
_logger.LogInformation("Cấu hình email đã đổi, kết nối lại {Host}", config.SmtpHost);
_config = config;
Reconnect(config);
});
}
public void Dispose() => _registration?.Dispose(); // PHẢI huỷ đăng ký
}
OnChange trả về một IDisposable — phải huỷ nó, nếu không bạn có rò rỉ tương tự như với event (bài 5.12).
Cảnh báo: OnChange có thể được gọi nhiều lần cho một lần thay đổi file, vì trình theo dõi file hệ điều hành đôi khi phát sinh nhiều sự kiện. Nếu callback làm việc tốn kém như kết nối lại, hãy khử trùng lặp:
private string _lastHost = "";
_registration = monitor.OnChange(config =>
{
if (config.SmtpHost == _lastHost) return; // không đổi thì bỏ qua
_lastHost = config.SmtpHost;
Reconnect(config);
});
7.13.5 — Xác thực cấu hình lúc khởi động
Cấu hình sai nên làm ứng dụng không khởi động được, thay vì lỗi lúc chạy vào 2 giờ sáng:
builder.Services
.AddOptions<EmailOptions>()
.Bind(builder.Configuration.GetSection("Email"))
.ValidateDataAnnotations()
.Validate(o => o.Port is > 0 and < 65536, "Port phải trong kho ảng 1-65535")
.ValidateOnStart(); // kiểm tra NGAY lúc khởi động
public class EmailOptions
{
[Required] public string SmtpHost { get; set; } = "";
[Range(1, 65535)] public int Port { get; set; }
[Range(1, 300)] public int TimeoutSeconds { get; set; } = 30;
}
Thiếu ValidateOnStart(), việc xác thực chỉ chạy khi có ai đó lần đầu yêu cầu IOptions<EmailOptions> — có thể là nhiều giờ sau khi triển khai, trong một luồng nghiệp vụ quan trọng.
Đây là một trong những cải thiện rẻ nhất có hiệu quả rõ nhất: một dòng, và mọi lỗi cấu hình lộ ra ngay lúc triển khai (bài 8.7).
7.13.6 — Rà lại code của bạn
Danh sách rà soát Options
- •Không tiêm IOptionsSnapshot vào service singleton.
- •Dùng IOptions làm mặc định, chỉ dùng Monitor khi cần đổi nóng.
- •IOptionsMonitor dùng CurrentValue, không dùng Value.
- •Biết nguồn cấu hình nào đọc lại được và nguồn nào không.
- •Đăng ký OnChange được huỷ trong Dispose.
- •Callback OnChange có khử trùng lặp nếu làm việc tốn kém.
- •Mọi lớp Options đều có ValidateOnStart.
- •Trường bắt buộc có Required, trường có khoảng hợp lệ có Range.
Bài tập áp dụng
Bài 1 — So sánh ba interface
Tạo ba service dùng ba interface khác nhau, đổi appsettings.json trong lúc chạy và ghi lại cái nào nhận giá trị mới.
Tiêu chí hoàn thành: bạn đo được cả ba hành vi, và biết chọn cái nào cho từng tình huống.
Gợi ý và lời giải — Bài 1
Gợi ý. Sửa đúng file mà ứng dụng đang đọc — file trong thư mục gốc nội dung, không phải bản sao trong thư mục build.
Lời giải:
builder.Services.Configure<CrmOptions>(builder.Configuration.GetSection("Crm"));
public class DocOptions(IOptions<CrmOptions> o, IOptionsMonitor<CrmOptions> m)
{
public string Options() => o.Value.TenHienThi;
public string Monitor() => m.CurrentValue.TenHienThi;
}
var doc = app.Services.GetRequiredService<DocOptions>();
Console.WriteLine($"trước khi đổi: IOptions={doc.Options()} | IOptionsMonitor={doc.Monitor()}");
File.WriteAllText(duongDanAppsettings,
"""{ "Crm": { "TenHienThi": "Đã đổi", "SoLeadToiDa": 250 } }""");
await Task.Delay(1500); // chờ file watcher
Console.WriteLine($"sau khi đổi : IOptions={doc.Options()} | IOptionsMonitor={doc.Monitor()}");
using var scope = app.Services.CreateScope();
var snap = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<CrmOptions>>();
Console.WriteLine($"IOptionsSnapshot (scope mới) = {snap.Value.TenHienThi}");
Kết quả chạy trên .NET 9.0.203:
trước khi đổi: IOptions=Ban đầu | IOptionsMonitor=Ban đầu
sau khi đổi : IOptions=Ban đầu | IOptionsMonitor=Đã đổi
IOptionsSnapshot (scope mới) = Đã đổi
| Interface | Nhận giá trị mới? | Vòng đời | Tiêm vào được |
|---|---|---|---|
IOptions<T> | Không | Singleton | Mọi nơi |
IOptionsSnapshot<T> | Có — mỗi scope đọc lại | Scoped | Chỉ Scoped và Transient |
IOptionsMonitor<T> | Có — ngay khi file đổi | Singleton | Mọi nơi |
Ba điều cần hiểu:
1. IOptions đọc một lần và giữ mãi.
Nó được đăng ký Singleton và giá trị được tính lúc đầu tiên có người hỏi.
-> đổi file không ảnh hưởng gì cho tới khi khởi động lại
Đây KHÔNG phải nhược đi ểm — với phần lớn cấu hình, đó là điều bạn muốn.
2. IOptionsSnapshot đọc lại một lần MỖI SCOPE.
Trong ASP.NET Core, một scope = một request
-> giá trị cố định trong suốt một request
-> request tiếp theo đọc lại
Điều này quan trọng: nếu cấu hình đổi giữa chừng một request,
bạn KHÔNG muốn nửa đầu dùng giá trị cũ và nửa sau dùng giá trị mới.
3. IOptionsMonitor là Singleton nhưng vẫn cập nhật.
Nó đăng ký lắng nghe file watcher và tự làm mới giá trị.
-> đọc CurrentValue bất kỳ lúc nào cũng được giá trị mới nhất
Đổi lại: giá trị có thể đổi GIỮA hai dòng code liên tiếp
// Nguy hiểm — hai lần đọc có thể cho hai giá trị khác nhau
if (_monitor.CurrentValue.BatTinhNang)
await LamGiAo(_monitor.CurrentValue.ThamSo); // cấu hình có thể đã đổi
// An toàn — chụp một lần
var cauHinh = _monitor.CurrentValue;
if (cauHinh.BatTinhNang)
await LamGiAo(cauHinh.ThamSo);
Chọn cái nào:
| Tình huống | Chọn |
|---|---|
| Chuỗi kết nối, URL, khoá API | IOptions — đổi thì khởi động lại |
| Cờ bật/tắt tính năng cần đổi nóng | IOptionsMonitor |
| Giá trị dùng trong một request, muốn cố định trong request đó | IOptionsSnapshot |
| Cần trong một Singleton | IOptions hoặc IOptionsMonitor — không bao giờ IOptionsSnapshot |
| Cần chạy code khi cấu hình đổi | IOptionsMonitor.OnChange |
_monitor.OnChange(cauHinh =>
{
_log.LogInformation("Cấu hình đổi, nạp lại bộ nhớ đệm");
_cache.Clear();
});
Lưu ý: OnChange có thể được gọi NHIỀU LẦN cho một lần sửa file,
vì trình soạn thảo ghi file theo nhiều bước.
-> callback phải idempotent, hoặc phải chống gọi dồn
Và một điều kiện dễ quên: cấu hình chỉ nạp lại được nếu nguồn hỗ trợ.
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
reloadOnChange mặc định là true cho appsettings.json trong WebApplication.CreateBuilder.
Nhưng với biến môi trường hay Azure Key Vault, nạp lại KHÔNG tự động
-> IOptionsMonitor vẫn không thấy gì đổi
Bài 2 — Tái hiện lỗi captive
Tiêm IOptionsSnapshot vào một singleton và đọc thông báo lỗi.
Tiêu chí hoàn thành: bạn có thông điệp lỗi chính xác, và biết điều gì xảy ra nếu kiểm tra bị tắt.
Gợi ý và lời giải — Bài 2
Gợi ý. Chạy hai lần: một lần có ValidateOnBuild, một lần không. Hai kết quả rất khác nhau, và bản không kiểm tra mới là bản đáng sợ.
Lời giải — có kiểm tra:
builder.Services.AddScoped<NguCanhYeuCau>();
builder.Services.AddSingleton<DichVuNen>(); // Singleton nhận Scoped
builder.Host.UseDefaultServiceProvider((_, o) =>
{
o.ValidateScopes = true;
o.ValidateOnBuild = true;
});
var app = builder.Build();
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'.)
Ứng dụng KHÔNG khởi động — và đó là kết quả tốt nhất có thể.
Lỗi xuất hiện:
- ngay lúc khởi động, không phải sau vài giờ chạy
- ở máy phát triển, không phải trên production
- với tên đúng của cả hai service, và đúng lý do
Và đây là bản không có kiểm tra:
builder.Host.UseDefaultServiceProvider((_, o) =>
{
o.ValidateScopes = false;
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()}");
}
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
Ứng dụng chạy bình thường. Không lỗi. Và luôn trả về dữ liệu sai.
Singleton giữ MỘT instance NguCanhYeuCau được tạo lúc nó khởi tạo.
Mọi scope sau đó tạo instance MỚI, nhưng singleton không bao giờ thấy chúng.
-> nó mãi mãi đọc TenantId=0
Đây gọi là captive dependency, và nó là loại lỗi tệ nhất trong DI:
Không ngoại lệ, không log, không cảnh báo.
Chỉ là dữ liệu sai — và trong hệ nhiều tenant, "dữ liệu sai"
nghĩa là dữ liệu của tenant này lọt sang tenant khác.
Vì sao container không tự báo mặc định:
ValidateScopes mặc định BẬT ở môi trường Development,
T ẮT ở Production — vì nó có chi phí kiểm tra ở mỗi lần phân giải.
Hệ quả: lỗi này chạy sạch trên máy phát triển nếu không ai tạo scope thủ công,
và lộ ra trên production ở dạng dữ liệu sai.
// Vì vậy: bật TƯỜNG MINH ở mọi môi trường
builder.Host.UseDefaultServiceProvider((_, o) =>
{
o.ValidateScopes = true; // kiểm tra mỗi lần phân giải
o.ValidateOnBuild = true; // kiểm tra TOÀN BỘ đồ thị lúc khởi động
});
ValidateOnBuild tốn vài chục mili giây lúc khởi động
và loại bỏ hẳn một lớp lỗi. Đổi này gần như luôn đáng.
Với IOptionsSnapshot thì lỗi cũng giống hệt:
public class DichVuNen(IOptionsSnapshot<CrmOptions> snapshot) { } // Singleton
Cannot consume scoped service 'IOptionsSnapshot`1[CrmOptions]'
from singleton 'DichVuNen'.
// Sửa: dùng IOptionsMonitor cho singleton
public class DichVuNen(IOptionsMonitor<CrmOptions> monitor) { }
Ba cách sửa captive dependency, tuỳ tình huống:
// 1. Đổi thứ được tiêm sang bản Singleton tương đương
IOptionsSnapshot<T> -> IOptionsMonitor<T>
// 2. Tiêm factory và tạo scope khi cần
public class DichVuNen(IServiceScopeFactory factory)
{
public async Task LamViecAsync(CancellationToken ct)
{
using var scope = factory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<CrmDbContext>();
await XuLyAsync(db, ct);
}
}
// 3. Xem lại: service này có thật sự cần là Singleton không?
builder.Services.AddScoped<DichVuNen>(); // đôi khi đây mới là cách đúng
Cách thứ ba đáng cân nhắc trước hai cách kia: rất nhiều service được đăng ký Singleton vì "cho nhanh", trong khi Scoped mới là vòng đời đúng với việc nó làm.
Bài 3 — Bật xác thực
Thêm ValidateOnStart cho một lớp Options, cố tình đặt giá trị sai, và xác nhận ứng dụng không khởi động.
Tiêu chí hoàn thành: bạn thấy ứng dụng dừng lại với danh sách đầy đủ mọi lỗi, và biết vì sao ValidateOnStart khác ValidateDataAnnotations đứng m ột mình.
Gợi ý và lời giải — Bài 3
Gợi ý. Không có ValidateOnStart, việc kiểm tra chỉ chạy lúc ai đó đọc .Value lần đầu — có thể là nhiều giờ sau khi khởi động.
Lời giải:
public class CrmOptions
{
[Required] public string TenHienThi { get; set; } = "";
[Range(1, 10_000)] public int SoLeadToiDa { get; set; }
}
builder.Services.AddOptions<CrmOptions>()
.Bind(builder.Configuration.GetSection("Crm"))
.ValidateDataAnnotations()
.Validate(o => o.SoLeadToiDa <= 1000, "SoLeadToiDa không được vượt 1000")
.ValidateOnStart();
Với cấu hình hợp lệ:
{ "Crm": { "TenHienThi": "Ban đầu", "SoLeadToiDa": 100 } }
Khởi động THÀNH CÔNG
Với cấu hình sai:
{ "Crm": { "TenHienThi": "", "SoLeadToiDa": 99999 } }
Kết quả chạy trên .NET 9.0.203:
OptionsValidationException:
DataAnnotation validation failed for 'CrmOptions' members: 'TenHienThi'
with the error: 'The TenHienThi field is required.'
| DataAnnotation validation failed for 'CrmOptions' members: 'SoLeadToiDa'
with the error: 'The field SoLeadToiDa must be between 1 and 10000.'
| SoLeadToiDa không được vượt 1000
Ba điều đáng chú ý trong thông điệp này:
1. Nó liệt kê MỌI lỗi, không dừng ở lỗi đầu tiên.
Ba lỗi trong một lần chạy
-> sửa hết một lượt, thay vì sửa một cái rồi chạy lại để thấy cái tiếp theo
2. Cả ValidateDataAnnotations lẫn Validate tuỳ biến đều chạy.
DataAnnotations cho ràng buộc đơn giản (bắt buộc, khoảng giá trị, độ dài)
Validate(...) cho quy tắc nghiệp vụ mà thuộc tính không diễn đạt được
3. Và ứng dụng KHÔNG khởi động — không có trạng thái nửa vời.
Không có ValidateOnStart:
ứng dụng khởi động bình thường
-> request đầu tiên chạm tới CrmOptions mới ném lỗi
-> có thể là ngay lập tức, có thể là sau nhiều giờ
-> và nếu đó là một endpoint ít dùng, có thể là sau nhiều NGÀY
Với ValidateOnStart:
pod không bao giờ vào trạng thái Ready
-> Kubernetes giữ pod cũ, không chuyển lưu lượng sang
-> deploy hỏng, nhưng hệ thống vẫn chạy
Đây là khác biệt thực tế lớn nhất: lỗi cấu hình trở thành lỗi triển khai thay vì lỗi lúc chạy.
Xác thực phức tạp hơn — dùng một lớp riêng:
public class CrmOptionsValidator : IValidateOptions<CrmOptions>
{
public ValidateOptionsResult Validate(string? name, CrmOptions o)
{
var loi = new List<string>();
if (o.SoLeadToiDa > 1000 && string.IsNullOrEmpty(o.ChuoiKetNoiBaoCao))
loi.Add("Hạn mức trên 1000 cần cấu hình database báo cáo riêng");
if (o.BatDongBo && o.ChuKyDongBo < TimeSpan.FromMinutes(1))
loi.Add("Chu kỳ đồng bộ tối thiểu 1 phút");
return loi.Count > 0
? ValidateOptionsResult.Fail(loi)
: ValidateOptionsResult.Success;
}
}
builder.Services.AddSingleton<IValidateOptions<CrmOptions>, CrmOptionsValidator>();
Lớp riêng dùng được khi:
- quy tắc liên quan tới NHIỀU thuộc tính cùng lúc
- cần tiêm phụ thuộc để kiểm tra (ví dụ kiểm tra một URL có phân giải được không)
- quy tắc dài hơn một biểu thức
Và một mẫu đáng có cho mọi cấu hình bắt buộc — thất bại sớm và rõ ràng:
// SAI — trả về null, lỗi xuất hiện ở một chỗ xa nguồn
var cs = builder.Configuration.GetConnectionString("Db");
builder.Services.AddDbContext<CrmDbContext>(o => o.UseSqlServer(cs));
// -> NullReferenceException hoặc ArgumentException ở đâu đó, khó lần
// ĐÚNG — nói rõ cái gì thiếu, ngay lúc khởi động
var cs = builder.Configuration.GetConnectionString("Db")
?? throw new InvalidOperationException(
"Thiếu ConnectionStrings:Db. Đặt trong appsettings hoặc biến môi trường.");
Danh sách nên xác thực lúc khởi động:
- Mọi chuỗi kết nối
- Mọi URL của dịch vụ bên ngoài (và kiểm tra nó là URI hợp lệ)
- Mọi khoá API và bí mật (kiểm tra có, không kiểm tra đúng)
- Mọi ngưỡng số (dương, trong khoảng hợp lý)
- Mọi đường dẫn tệp (tồn tại và ghi được, nếu cần ghi)
Chi phí: vài chục mili giây lúc khởi động
Đổi lại: mọi lỗi cấu hình lộ ra trước khi có request đầu tiên
Nối lại ba bài: bài 1 cho thấy ba interface đọc cấu hình ở ba thời điểm khác nhau, bài 2 cho thấy chọn sai interface trong Singleton gây sai dữ liệu âm thầm, bài 3 cho thấy cách biến lỗi cấu hình thành lỗi khởi động. Cả ba đều đi theo cùng một hướng: đẩy lỗi về càng sớm và càng ồn ào càng tốt.
Tự kiểm tra
Frequently asked questions
Ba interface Options khác nhau thế nào?
IOptions là singleton và đọc một lần lúc khởi động. IOptionsSnapshot là scoped và đọc lại mỗi request. IOptionsMonitor là singleton nhưng luôn trả giá trị mới nhất qua CurrentValue.
Vì sao không tiêm được IOptionsSnapshot vào singleton?
Vì nó là scoped, nên tiêm vào singleton tạo ra captive dependency. DI container sẽ báo lỗi cannot consume scoped service from singleton. Với singleton, dùng IOptions hoặc IOptionsMonitor.
Nguồn cấu hình nào không đọc lại được?
Biến môi trường và tham số dòng lệnh chỉ đọc một lần lúc khởi động. Azure Key Vault chỉ đọc lại nếu đặt ReloadInterval. File appsettings.json và user secrets thì đọc lại được.
Vì sao đổi biến môi trường của container mà IOptionsMonitor không nhận?
Vì biến môi trường được đọc một lần lúc tiến trình khởi động và không có cơ chế theo dõi thay đổi. Phải khởi động lại container.
Vì sao callback OnChange cần khử trùng lặp?
Vì trình theo dõi file của hệ điều hành đôi khi phát sinh nhiều sự kiện cho một lần thay đổi, nên callback có thể chạy nhiều lần. Nếu nó làm việc tốn kém như kết nối lại thì cần kiểm tra giá trị có thực sự đổi không.
ValidateOnStart giải quyết vấn đề gì?
Không có nó, việc xác thực chỉ chạy khi có ai đó lần đầu yêu cầu Options, có thể là nhiều giờ sau khi triển khai trong một luồng nghiệp vụ quan trọng. Có nó, mọi lỗi cấu hình lộ ra ngay lúc khởi động.
Kết luận
Ba điều đáng nhớ nhất:
IOptionsSnapshotlà scoped — không bao giờ tiêm vào singleton.- Biến môi trường không đ ọc lại được, nên
IOptionsMonitorcũng không giúp gì ở đó. ValidateOnStart()là một dòng đổi lỗi lúc 2 giờ sáng thành lỗi lúc triển khai.
Tham khảo
- Options pattern trong .NET
- Dependency injection trong .NET
- Hướng dẫn dependency injection
- ASP.NET Core fundamentals
Điều hướng
- Bài trước: 7.11 — Mini case study
- Bài tiếp theo: 7.13 — Review and Assessment
- Về module: Trang mục lục