Nguon: Microsoft Learn · .NET 8.0

Dependency Injection (tiêm phụ thuộc) trong ASP.NET Core

Nguồn: Dependency injection in 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:

csharp
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:

DI giải quyết các vấn đề này thông qua:

Trong ví dụ sau, interface IMyDependency định nghĩa signature (chữ ký) của phương thức WriteMessage.

Interfaces/IMyDependency.cs:

csharp
public interface IMyDependency
{
    void WriteMessage(string message);
}

Interface trên được triển khai bởi kiểu cụ thể MyDependency.

Services/MyDependency.cs:

csharp
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:

csharp
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:

razor
@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:

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:

csharp
public class MyDependency(ILogger<MyDependency> logger) : IMyDependency
{
    public void WriteMessage(string message)
    {
        logger.LogInformation($"MyDependency.WriteMessage: {message}");
    }
}

MyDependency phụ thuộc vào ILogger&lt;TCategoryName&gt;, 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:

csharp
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>.

csharp
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.

csharp
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:

csharp
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:

csharp
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:

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:

csharp
public interface IStringCache
{
    string Get(int key);
}

Services/StringCache1.cs:

csharp
public class StringCache1 : IStringCache
{
    public string Get(int key) => $"Resolving {key} from StringCache1.";
}

Services/StringCache2.cs:

csharp
public class StringCache2 : IStringCache
{
    public string Get(int key) => $"Resolving {key} from StringCache2.";
}

Đăng ký và sử dụng keyed services trong file Program:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

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:

csharp
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:

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:

csharp
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:

csharp
// Không làm theo cách này
var service = HttpContext.RequestServices.GetService<IMyService>();

Đúng:

csharp
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 ServiceLifetime
IApplicationBuilderFactoryTransient
IHostApplicationLifetimeSingleton
IWebHostEnvironmentSingleton
IStartupSingleton
IStartupFilterTransient
IServerSingleton
IHttpContextFactoryTransient
ILogger&lt;TCategoryName&gt;Singleton
ILoggerFactorySingleton
ObjectPoolProviderSingleton
IConfigureOptions&lt;TOptions&gt;Transient
IOptions&lt;TOptions&gt;Singleton
DiagnosticSourceSingleton
DiagnosticListenerSingleton