Mục lục
Tránh những lỗi thường gặp với Dependency Injection và xây dựng các tác vụ nền sạch sẽ, đáng tin cậy trong .NET bằng IServiceScopeFactory
Tác giả: Yohan Malshika
Ngày: 19 tháng 10, 2025
Khi xây dựng ứng dụng .NET, bạn có thể cần chạy các quy trình nền làm việc độc lập với các yêu cầu của người dùng. Ví dụ bao gồm gửi email định kỳ, đồng bộ dữ liệu với hệ thống bên ngoài, hoặc dọn dẹp các bản ghi cũ từ cơ sở dữ liệu. Cách được khuyến nghị để xử lý công việc nền như vậy trong .NET là sử dụng Hosted Service.
Tuy nhiên, một vấn đề phổ biến mà các nhà phát triển gặp phải khi làm việc với hosted services là sử dụng scoped services bên trong chúng. Đây cũng là một câu hỏi phỏng vấn phổ biến cho các nhà phát triển .NET. Ví dụ, nếu tác vụ nền của bạn cần truy cập một DbContext, một repository hoặc bất kỳ service nào được đăng ký dưới dạng scoped, bạn sẽ nhanh chóng phát hiện ra rằng bạn không thể inject nó trực tiếp vào một hosted service. Hạn chế này tồn tại vì cách mà container dependency injection (DI) trong .NET quản lý vòng đời của services.
Hãy cùng xem tại sao vấn đề này xảy ra, và sau đó chúng ta sẽ đi qua một giải pháp chính xác với một ví dụ chi tiết.
Hiểu Về Vòng Đời Dịch Vụ Trong .NET
Trước khi đi sâu vào giải pháp, điều quan trọng là phải hiểu ba vòng đời dịch vụ chính trong hệ thống DI của .NET:
- Singleton: Được tạo một lần và tồn tại trong suốt vòng đời của ứng dụng
- Scoped: Được tạo mới cho mỗi scope (thường là mỗi HTTP request trong ứng dụng web)
- Transient: Được tạo mới mỗi khi được yêu cầu
Hosted services được đăng ký như một singleton, trong khi nhiều service quan trọng như DbContext trong Entity Framework được đăng ký như scoped services. Đây chính là nguồn gốc của vấn đề: một singleton không thể tiêm một scoped service trực tiếp.
Vấn Đề: Scoped Services Trong Singleton
Hãy xem xét ví dụ đơn giản này về một hosted service cố gắng sử dụng một repository:
<br>public class MyBackgroundService : BackgroundService<br>
{<br>
private readonly IMyRepository _repository; // Bị lỗi!<br>
<br>
public MyBackgroundService(IMyRepository repository)<br>
{<br>
_repository = repository;<br>
}<br>
<br>
// ...<br>
}<br>
Đoạn code trên sẽ ném ra một exception trong thời gian chạy, bởi vì .NET DI container từ chối giải quyết một scoped service từ một singleton context. Điều này ngăn chặn các vấn đề xảy ra khi một scoped service bị giữ quá lâu và có thể dẫn đến rò rỉ bộ nhớ hoặc các lỗi về trạng thái.
Giải Pháp: Sử Dụng IServiceScopeFactory
Giải pháp chính xác là sử dụng IServiceScopeFactory để tạo ra một scope mới trong phương thức ExecuteAsync của hosted service. Cách tiếp cận này cho phép bạn truy cập vào scoped services một cách an toàn bên trong vòng đời của hosted service.
Dưới đây là một ví dụ thực tế:
<br>public class EmailBackgroundService : BackgroundService<br>
{<br>
private readonly IServiceScopeFactory _serviceScopeFactory;<br>
private readonly ILogger<EmailBackgroundService> _logger;<br>
<br>
public EmailBackgroundService(<br>
IServiceScopeFactory serviceScopeFactory,<br>
ILogger<EmailBackgroundService> logger)<br>
{<br>
_serviceScopeFactory = serviceScopeFactory;<br>
_logger = logger;<br>
}<br>
<br>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)<br>
{<br>
while (!stoppingToken.IsCancellationRequested)<br>
{<br>
using (var scope = _serviceScopeFactory.CreateScope())
{<br>
// Lấy scoped services từ scope mới<br>
var emailService = scope.ServiceProvider<br>
.GetRequiredService<IEmailService>();<br>
var dbContext = scope.ServiceProvider<br>
.GetRequiredService<ApplicationDbContext>();<br>
<br>
// Thực hiện công việc với scoped services<br>
await ProcessPendingEmails(emailService, dbContext);<br>
}<br>
<br>
// Chờ 30 giây trước khi chạy lần tiếp theo<br>
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);<br>
}<br>
}<br>
<br>
private async Task ProcessPendingEmails(<br>
IEmailService emailService, <br>
ApplicationDbContext dbContext)<br>
{<br>
// Logic xử lý email ở đây<br>
var pendingEmails = await dbContext.PendingEmails<br>
.Where(e => !e.IsSent)<br>
.ToListAsync();<br>
<br>
foreach (var email in pendingEmails)<br>
{<br>
await emailService.SendEmailAsync(email);<br>
email.IsSent = true;<br>
}<br>
<br>
await dbContext.SaveChangesAsync();<br>
}<br>
}<br>
Tại Sao Điều Này Hoạt Động?
Cách tiếp cận này hoạt động vì một số lý do quan trọng:
- An toàn vòng đời: Mỗi iteration của vòng lặp tạo một scope mới, đảm bảo rằng scoped services được giải phóng đúng cách sau khi sử dụng
- Không có rò rỉ bộ nhớ: Việc sử dụng
usingstatement đảm bảo rằng scope và tất cả các scoped services bị dispose khi hoàn thành - Tính độc lập: Mỗi iteration hoạt động độc lập, giống như một HTTP request riêng biệt
Các Mẫu Thực Tế Khác
1. Xử Lý Lỗi Một Cách Thanh Lịch
<br>protected override async Task ExecuteAsync(CancellationToken stoppingToken)<br>
{<br>
while (!stoppingToken.IsCancellationRequested)<br>
{<br>
try<br>
{<br>
using (var scope = _serviceScopeFactory.CreateScope())
{<br>
var service = scope.ServiceProvider<br>
.GetRequiredService<IMyService>();<br>
await service.ProcessAsync();<br>
}<br>
}<br>
catch (Exception ex)<br>
{<br>
_logger.LogError(ex, "Lỗi khi xử lý tác vụ nền");<br>
}<br>
<br>
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);<br>
}<br>
}<br>
2. Xử Lý Hàng Loạt (Batch Processing)
<br>private async Task ProcessBatchAsync(<br>
IMyRepository repository, <br>
int batchSize = 100)<br>
{<br>
var items = await repository.GetPendingItemsAsync(batchSize);<br>
<br>
foreach (var item in items)<br>
{<br>
// Xử lý từng item<br>
await repository.ProcessItemAsync(item);<br>
}<br>
<br>
await repository.SaveChangesAsync();<br>
}<br>
Các Thư Viện Hữu Ích
Cộng đồng .NET cũng đã phát triển các thư viện để đơn giản hóa mẫu này. Một ví dụ là Scopist – một gói NuGet mã nguồn mở nhỏ cung cấp ScopedResolver<T> generic để an toàn giải quyết scoped services từ singleton.
Best Practices
- Luôn sử dụng using statement: Đảm bảo scope được dispose đúng cách
- Xử lý lỗi: Bao bọc logic của bạn trong try-catch để ngăn hosted service dừng lại do exception
- Logging: Ghi lại các lỗi và thông tin để dễ dàng debug
- Cấu hình độ trễ: Sử dụng cấu hình cho các khoảng thời gian chờ thay vì hardcode
- Kiểm thử: Tạo unit tests cho hosted services của bạn bằng cách mock
IServiceScopeFactory
Kết Luận
Sử dụng scoped services bên trong hosted services trong .NET không phải là một thách thức nếu bạn hiểu cách hoạt động của hệ thống DI. Bằng cách sử dụng IServiceScopeFactory để tạo ra các scope mới trong phương thức ExecuteAsync, bạn có thể truy cập an toàn vào các dịch vụ như DbContext, repositories, và các scoped services khác mà không gặp vấn đề về vòng đời.
Mẫu này không chỉ giải quyết vấn đề kỹ thuật mà còn tạo ra các hosted services sạch sẽ, có thể bảo trì và có thể kiểm thử được. Nó cho phép bạn xây dựng các tác vụ nền mạnh mẽ cho các trường hợp sử dụng như xử lý hàng loạt, gửi thông báo, dọn dẹp dữ liệu và nhiều hơn nữa.
Hãy nhớ: Khi làm việc với hosted services, hãy luôn tôn trọng vòng đời của services và sử dụng IServiceScopeFactory khi bạn cần truy cập vào scoped services từ một singleton context.



