Dependency Injection (tiêm phụ thuộc) trong ASP.NET Core
ASP.NET Core hỗ trợ mẫu thiết kế phần mềm dependency injection (DI) (tiêm phụ thuộc), là kỹ thuật để đạt được Inversion of Control (IoC) (đảo ngược điều khiển) giữa các lớp và các phụ thuộc của chúng.
Tổng quan về Dependency Injection
Một dependency (phụ thuộc) là một đối tượng mà đối tượng khác phụ thuộc vào. Xem xét lớp MyDependency sau với phương thức WriteMessage:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage: {message}");
}
}Một lớp có thể tạo một instance (thể hiện) của lớp MyDependency để gọi phương thức WriteMessage của nó. Việc lấy phụ thuộc trực tiếp như trong ví dụ trước là có vấn đề và nên tránh vì những lý do sau:
- Để thay thế
MyDependencybằng một triển khai khác, lớp sử dụng phải được sửa đổi. - Nếu
MyDependencycó các phụ thuộc, chúng cũng phải được cấu hình bởi lớp sử dụng. Trong một dự án lớn với nhiều lớp phụ thuộc vàoMyDependency, code cấu hình trở nên rải rác trong toàn bộ ứng dụng. - Việc triển khai này khó unit test.
DI giải quyết các vấn đề này thông qua:
- Sử dụng interface (giao diện) hoặc lớp cơ sở để trừu tượng hóa việc triển khai phụ thuộc.
- Đăng ký phụ thuộc trong service container (bộ chứa service), còn gọi là DI container. ASP.NET Core cung cấp service container tích hợp sẵn, IServiceProvider. Services thường được đăng ký trong file
Programcủa ứng dụng. - Injection (tiêm) service vào các lớp nơi nó được sử dụng. Framework tạo các instance phụ thuộc và hủy chúng khi không còn cần thiết.
Trong ví dụ sau, interface IMyDependency định nghĩa signature (chữ ký) của phương thức WriteMessage.
Interfaces/IMyDependency.cs:
public interface IMyDependency
{
void WriteMessage(string message);
}Interface trên được triển khai bởi kiểu cụ thể MyDependency.
Services/MyDependency.cs:
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage: {message}");
}
}Ứng dụng đăng ký service IMyDependency với kiểu cụ thể MyDependency. Phương thức AddScoped đăng ký service với lifetime (thời gian sống) scoped:
builder.Services.AddScoped<IMyDependency, MyDependency>();
Service IMyDependency được yêu cầu và sử dụng để gọi phương thức WriteMessage. Ví dụ trong Razor component:
@page "/dependency-example-2"
@inject IMyDependency Dependency
<button @onclick="WriteMessage">Write message</button>
@code {
private void WriteMessage() =>
Dependency.WriteMessage("DependencyExample2.WriteMessage called");
}Bằng cách sử dụng mẫu DI, lớp sử dụng phụ thuộc:
- Không sử dụng kiểu cụ thể
MyDependency, chỉ dùng interfaceIMyDependencymà nó triển khai. Điều đó giúp dễ dàng thay đổi triển khai mà không cần sửa đổi phía sử dụng. - Không trực tiếp tạo instance
MyDependencyhay hủy nó. Phụ thuộc được tạo và hủy bởi service container.
Triển khai IMyDependency có thể được cải thiện bằng cách sử dụng logging API tích hợp sẵn:
Services/MyDependency.cs:
public class MyDependency(ILogger<MyDependency> logger) : IMyDependency
{
public void WriteMessage(string message)
{
logger.LogInformation($"MyDependency.WriteMessage: {message}");
}
}MyDependency phụ thuộc vào ILogger<TCategoryName>, một service do framework cung cấp.
Thông thường DI được sử dụng theo chuỗi. Mỗi phụ thuộc được yêu cầu lần lượt yêu cầu các phụ thuộc của chính nó. Container giải quyết các phụ thuộc trong graph và trả về service đã được giải quyết hoàn toàn. Tập hợp các phụ thuộc cần giải quyết thường được gọi là dependency tree, dependency graph, hoặc object graph.
Các phương thức đăng ký Service
Để biết thêm về đăng ký service, xem Service registration.
Đăng ký service với chỉ kiểu triển khai tương đương với đăng ký service với cùng kiểu triển khai và kiểu service:
builder.Services.AddSingleton<MyDependency>();
Các phương thức đăng ký service có thể được sử dụng để đăng ký nhiều instance service của cùng kiểu service. Khi AddSingleton được gọi hai lần với IMyDependency là kiểu service, lần gọi thứ hai sẽ ghi đè lần trước khi giải quyết IMyDependency, nhưng thêm vào lần trước khi nhiều service được giải quyết qua IEnumerable<IMyDependency>.
builder.Services.AddSingleton<IMyDependency, MyDependency>();
builder.Services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}Đăng ký nhóm Services với Extension Methods (phương thức mở rộng)
Quy ước của ASP.NET Core framework để đăng ký một nhóm các service liên quan là sử dụng một phương thức mở rộng Add{GROUP NAME} để đăng ký tất cả các service được yêu cầu bởi một tính năng framework. Ví dụ, phương thức mở rộng AddRazorComponents đăng ký các service cần thiết cho server-side rendering của Razor components.
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));
builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();Các đăng ký liên quan có thể được chuyển vào phương thức mở rộng để đăng ký service:
namespace Microsoft.Extensions.DependencyInjection;
public static class ConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}Sử dụng:
builder.Services
.AddConfig(builder.Configuration)
.AddDependencyGroup();Service Lifetimes (thời gian sống của service)
Để biết thêm về service lifetimes, xem Service lifetimes.
Để sử dụng scoped services trong middleware, hãy dùng một trong các cách tiếp cận sau:
- Inject service vào phương thức
InvokehoặcInvokeAsynccủa middleware. Sử dụng constructor injection (tiêm qua constructor) sẽ ném ngoại lệ runtime vì nó buộc scoped service hoạt động như singleton. - Sử dụng factory-based middleware. Middleware được đăng ký theo cách này được kích hoạt cho mỗi client request, cho phép scoped services được inject vào constructor của middleware.
Keyed Services (service có khóa)
Keyed services đăng ký và lấy services bằng cách sử dụng keys (khóa). Một service được liên kết với một key bằng cách gọi bất kỳ phương thức mở rộng đăng ký service nào sau đây:
Ví dụ về keyed services với các interface và implementation:
Interfaces/IStringCache.cs:
public interface IStringCache
{
string Get(int key);
}Services/StringCache1.cs:
public class StringCache1 : IStringCache
{
public string Get(int key) => $"Resolving {key} from StringCache1.";
}Services/StringCache2.cs:
public class StringCache2 : IStringCache
{
public string Get(int key) => $"Resolving {key} from StringCache2.";
}Đăng ký và sử dụng keyed services trong file Program:
builder.Services.AddKeyedSingleton<IStringCache, StringCache1>("cache1");
builder.Services.AddKeyedSingleton<IStringCache, StringCache2>("cache2");
...
app.MapGet("/cache1", ([FromKeyedServices("cache1")] IStringCache stringCache1) =>
stringCache1.Get(1));
app.MapGet("/cache2", ([FromKeyedServices("cache2")] IStringCache stringCache2) =>
stringCache2.Get(2));Thời gian sống và tùy chọn đăng ký
Để minh họa sự khác biệt giữa service lifetimes và các tùy chọn đăng ký của chúng, hãy xem xét các interface đại diện cho một tác vụ như một hoạt động với identifier (mã định danh) OperationId:
IOperation.cs:
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }Lớp Operation sau triển khai tất cả các interface trên. Constructor Operation tạo một GUID và lưu bốn ký tự cuối trong thuộc tính OperationId:
Operation.cs:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}Đăng ký nhiều lần lớp Operation theo các lifetime được đặt tên:
builder.Services.AddTransient<IOperationTransient, Operation>(); builder.Services.AddScoped<IOperationScoped, Operation>(); builder.Services.AddSingleton<IOperationSingleton, Operation>();
Kết quả đầu ra từ các ví dụ trên cho thấy:
- Transient objects luôn khác nhau. Giá trị
OperationIdtransient khác nhau cho Razor component và trong middleware. - Scoped objects giống nhau cho một request nhất định nhưng khác nhau giữa các Blazor circuit mới.
- Singleton objects giống nhau cho mọi request hoặc Blazor circuit.
Giải quyết Service khi khởi động ứng dụng
Code sau minh họa cách giải quyết scoped service trong thời gian giới hạn khi ứng dụng khởi động:
var app = builder.Build();
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var dependency = services.GetRequiredService<IMyDependency>();
dependency.WriteMessage("Call services from main");
}Request Services
Services và các phụ thuộc của chúng trong một ASP.NET Core request được expose (tiếp xúc) qua HttpContext.RequestServices.
Framework tạo một scope per request, và RequestServices expose scoped service provider. Tất cả scoped services có hiệu lực chừng nào request vẫn đang hoạt động.
Ưu tiên yêu cầu các phụ thuộc dưới dạng tham số constructor hơn là giải quyết service từ RequestServices. Yêu cầu phụ thuộc dưới dạng tham số constructor tạo ra các lớp dễ kiểm thử hơn.
Thiết kế services cho Dependency Injection
Khi thiết kế service cho DI:
- Tránh các lớp và thành viên static (tĩnh) có trạng thái. Tránh tạo global state bằng cách thiết kế ứng dụng sử dụng singleton services.
- Tránh khởi tạo trực tiếp các lớp phụ thuộc trong services.
- Làm cho services nhỏ, được thiết kế tốt và dễ kiểm thử.
Nếu một lớp có quá nhiều phụ thuộc được inject, đó có thể là dấu hiệu cho thấy lớp có quá nhiều trách nhiệm và vi phạm Single Responsibility Principle (SRP) (Nguyên tắc Trách nhiệm Đơn).
Hủy bỏ Services (Disposal)
Container gọi Dispose cho các kiểu IDisposable mà nó tạo ra. Services được giải quyết từ container không bao giờ nên bị developer hủy. Nếu kiểu hoặc factory được đăng ký là singleton, container tự động hủy singleton.
Services/Service1.cs:
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
{
return;
}
Console.WriteLine("Service1.Dispose");
_disposed = true;
GC.SuppressFinalize(this);
}
}Thay thế Service Container mặc định
Để biết thêm, xem Dependency injection in .NET: Default service container replacement.
Khuyến nghị
Tránh sử dụng service locator pattern (mẫu vị trí service). Ví dụ, đừng gọi GetService để lấy service instance khi bạn có thể dùng DI:
Không đúng:
// Không làm theo cách này var service = HttpContext.RequestServices.GetService<IMyService>();
Đúng:
public class MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
{
public void MyMethod()
{
var option = optionsMonitor.CurrentValue.Option;
...
}
}Tránh static access (truy cập tĩnh) vào HttpContext (ví dụ, IHttpContextAccessor.HttpContext).
DI là giải pháp thay thế cho các mẫu truy cập đối tượng static/global.
Các Services do Framework cung cấp
File Program đăng ký các services mà ứng dụng sử dụng, bao gồm các tính năng nền tảng như Entity Framework Core. Dưới đây là một mẫu nhỏ của các services được framework đăng ký:
| Kiểu Service | Lifetime |
|---|---|
| IApplicationBuilderFactory | Transient |
| IHostApplicationLifetime | Singleton |
| IWebHostEnvironment | Singleton |
| IStartup | Singleton |
| IStartupFilter | Transient |
| IServer | Singleton |
| IHttpContextFactory | Transient |
| ILogger<TCategoryName> | Singleton |
| ILoggerFactory | Singleton |
| ObjectPoolProvider | Singleton |
| IConfigureOptions<TOptions> | Transient |
| IOptions<TOptions> | Singleton |
| DiagnosticSource | Singleton |
| DiagnosticListener | Singleton |