ASP.NET Core: Dependency Injection trong .NET
Trong bài viết này
- Nhiều quy tắc khám phá hàm tạo
- Đăng ký nhóm dịch vụ bằng phương thức mở rộng
- Dịch vụ do framework cung cấp
- Tuổi thọ dịch vụ
- Phương thức đăng ký dịch vụ
- Xác thực phạm vi
- Kịch bản phạm vi
- Dịch vụ có khóa
- Xem thêm
.NET hỗ trợ mẫu thiết kế phần mềm Dependency Injection (DI), đây là một kỹ thuật để đạt được Đảo ngược điều khiển (IoC) giữa các lớp và các phần phụ thuộc của chúng. Tính năng DI trong .NET là một phần tích hợp sẵn của framework, cùng với cấu hình (configuration), ghi nhật ký (logging) và mẫu tùy chọn (options pattern).
Dependency (Phụ thuộc) là một đối tượng mà một đối tượng khác phụ thuộc vào. Kiểm tra lớp MessageWriter
sau đây bằng phương thức Write
mà các lớp khác phụ thuộc vào:
public class MessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Một lớp có thể tạo một thể hiện của lớp MessageWriter
đó để sử dụng phương thức Write
của nó. Trong ví dụ sau, lớp MessageWriter
là phần phụ thuộc của lớp Worker
:
public class Worker : BackgroundService
{
private readonly MessageWriter _messageWriter = new MessageWriter();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1000, stoppingToken);
}
}
}
Lớp tạo và phụ thuộc trực tiếp vào lớp MessageWriter
. Các phần phụ thuộc được mã hóa cứng, chẳng hạn như trong ví dụ trên, có vấn đề và cần tránh vì những lý do sau:
- Để thay thế
MessageWriter
bằng một cách triển khai khác, lớpWorker
này phải được sửa đổi. - Nếu
MessageWriter
có phần phụ thuộc thì chúng cũng phải được cấu hình bởi lớpWorker
. Trong một dự án lớn có nhiều lớp tùy thuộc vàoMessageWriter
, mã cấu hình sẽ nằm rải rác trên ứng dụng. - Việc thực hiện này rất khó để kiểm tra đơn vị (unit test). Ứng dụng nên sử dụng lớp
MessageWriter
mô phỏng hoặc lớp sơ khai, điều này là không thể với phương pháp này.
Tính năng DI giải quyết những vấn đề này thông qua:
- Việc sử dụng một interface hoặc lớp cơ sở để trừu tượng hóa việc triển khai phần phụ thuộc.
- Đăng ký phần phụ thuộc trong vùng chứa dịch vụ. .NET cung cấp một bộ chứa dịch vụ tích hợp sẵn đó là IServiceProvider. Các dịch vụ thường được đăng ký khi khởi động ứng dụng và được thêm vào IServiceCollection. Sau khi tất cả các dịch vụ được thêm vào, bạn sử dụng BuildServiceProvider để tạo vùng chứa dịch vụ.
- Đưa (Injection) dịch vụ vào hàm tạo của lớp nơi nó được sử dụng. Framework này đảm nhận trách nhiệm tạo một phiên bản phụ thuộc và loại bỏ nó khi không còn cần thiết nữa.
Ví dụ: Interface IMessageWriter
định nghĩa Write
:
namespace DependencyInjection.Example;
public interface IMessageWriter
{
void Write(string message);
}
Interface này được thực hiện bởi một kiểu cụ thể là MessageWriter
:
namespace DependencyInjection.Example;
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Code mẫu đăng ký dịch vụ IMessageWriter
với kiểu cụ thể MessageWriter
. Phương thức AddSingleton đăng ký dịch vụ với thời gian tồn tại đơn lẻ, thời gian tồn tại của một yêu cầu. Thời gian sử dụng dịch vụ được mô tả sau trong bài viết này.
using DependencyInjection.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
host.Run();
Trong đoạn code trên, ứng dụng mẫu:
- Tạo một phiên bản trình tạo ứng dụng máy chủ.
- Cấu hình các dịch vụ bằng cách đăng ký:
Worker
như một dịch vụ được lưu trữ. Để biết thêm thông tin, hãy xem Service Worker trong .NET.- Interface
IMessageWriter
như một dịch vụ đơn lẻ với cách triển khai lớpMessageWriter
tương ứng.
- Xây dựng máy chủ và chạy nó.
Máy chủ chứa nhà cung cấp dịch vụ DI. Nó cũng chứa tất cả các dịch vụ liên quan khác cần thiết để tự động khởi tạo Worker
và cung cấp cho IMessageWriter
cách triển khai tương ứng làm đối số.
namespace DependencyInjection.Example;
public sealed class Worker : BackgroundService
{
private readonly IMessageWriter _messageWriter;
public Worker(IMessageWriter messageWriter) =>
_messageWriter = messageWriter;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
Bằng cách sử dụng mẫu DI, service worker:
- Không sử dụng kiểu cụ thể
MessageWriter
, chỉ sử dụng interfaceIMessageWriter
triển khai nó. Điều đó giúp dễ dàng thay đổi cách triển khai mà service worker sử dụng mà không cần sửa đổi service worker. - Không tạo một phiên bản của
MessageWriter
. Phiên bản này được tạo bởi bộ chứa DI.
Việc triển khai interface IMessageWriter
có thể được cải thiện bằng cách sử dụng API ghi nhật ký tích hợp:
namespace DependencyInjection.Example;
public class LoggingMessageWriter : IMessageWriter
{
private readonly ILogger<LoggingMessageWriter> _logger;
public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
_logger = logger;
public void Write(string message) =>
_logger.LogInformation("Info: {Msg}", message);
}
Phương thức đã cập nhật AddSingleton
đăng ký triển khai mới IMessageWriter
:
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>());
Kiểu HostApplicationBuilder (builder) là một phần của gói NuGet Microsoft.Extensions.Hosting
.
LoggingMessageWriter
phụ thuộc vào ILogger<TCategoryName> mà nó yêu cầu trong hàm tạo. ILogger<TCategoryName>
là một dịch vụ được cung cấp theo khuôn framework.
Không có gì lạ khi sử dụng tính năng chèn phụ thuộc theo kiểu chuỗi. Mỗi phần phụ thuộc được yêu cầu lần lượt yêu cầu các phần phụ thuộc riêng của nó. Vùng chứa giải quyết các phần phụ thuộc trong biểu đồ và trả về dịch vụ được giải quyết đầy đủ. Tập hợp các phụ thuộc phải được giải quyết thường được gọi là cây phụ thuộc, biểu đồ phụ thuộc hoặc biểu đồ đối tượng.
Vùng chứa giải quyết ILogger<TCategoryName>
bằng cách tận dụng các kiểu mở (generic), loại bỏ nhu cầu đăng ký mọi kiểu được xây dựng (generic).
Với thuật ngữ DI, một dịch vụ:
- Thông thường là một đối tượng cung cấp dịch vụ cho các đối tượng khác, chẳng hạn như dịch vụ
IMessageWriter
. - Không liên quan đến dịch vụ web, mặc dù dịch vụ này có thể sử dụng dịch vụ web.
Framework này cung cấp một hệ thống ghi nhật ký mạnh mẽ. Việc IMessageWriter
triển khai trong các ví dụ trước được viết để minh họa DI cơ bản chứ không phải để triển khai ghi nhật ký. Hầu hết các ứng dụng không cần phải ghi nhật ký. Đoạn code sau đây minh họa cách sử dụng tính năng ghi nhật ký mặc định, chỉ yêu cầu Worker
được đăng ký làm dịch vụ được lưu trữ trên máy chủ AddHostedService:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger) =>
_logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1_000, stoppingToken);
}
}
}
Sử dụng đoạn code trên, ta không cần phải cập nhật Program.cs
vì việc ghi nhật ký được cung cấp bởi framework.
Nhiều quy tắc khám phá hàm tạo
Khi một kiểu định nghĩa nhiều hơn một hàm tạo, nhà cung cấp dịch vụ có logic để xác định nên sử dụng hàm tạo nào. Hàm tạo có nhiều tham số nhất trong đó các kiểu có thể giải quyết DI được chọn. Hãy xem xét dịch vụ ví dụ C# sau:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// omitted for brevity
}
public ExampleService(FooService fooService, BarService barService)
{
// omitted for brevity
}
}
Trong đoạn code trên, giả sử rằng việc ghi nhật ký đã được thêm và có thể phân giải được từ nhà cung cấp dịch vụ nhưng các kiểu FooService
và BarService
thì không. Hàm tạo với tham số ILogger<ExampleService>
được sử dụng để giải quyết thể hiện của ExampleService
. Mặc dù có một hàm tạo định nghĩa nhiều tham số hơn nhưng các kiểu FooService
và BarService
không thể giải quyết DI.
Nếu có sự mơ hồ khi khám phá các hàm tạo, một ngoại lệ sẽ được đưa ra. Hãy xem xét dịch vụ ví dụ C# sau:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// omitted for brevity
}
public ExampleService(IOptions<ExampleOptions> options)
{
// omitted for brevity
}
}
Cảnh báo
Đoạn code
ExampleService
có tham số kiểu có thể phân giải DI không rõ ràng sẽ đưa ra một ngoại lệ. Đừng làm điều này—nó nhằm mục đích hiển thị ý nghĩa của "các loại DI-có thể phân giải mơ hồ".
Trong ví dụ trên có ba hàm tạo. Hàm tạo đầu tiên không có tham số và không yêu cầu dịch vụ nào từ nhà cung cấp dịch vụ. Giả sử rằng cả ghi nhật ký và tùy chọn đã được thêm vào vùng chứa DI và là các dịch vụ có thể giải quyết DI. Khi bộ chứa DI cố gắng giải quyết kiểu ExampleService
, thì nó sẽ đưa ra một ngoại lệ, vì hai hàm tạo không rõ ràng.
Thay vào đó, bạn có thể tránh sự mơ hồ bằng cách định nghĩa một hàm tạo chấp nhận cả hai kiểu có thể giải quyết DI:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// omitted for brevity
}
}
Đăng ký nhóm dịch vụ bằng phương thức mở rộng
Microsoft Extensions sử dụng quy ước để đăng ký một nhóm dịch vụ liên quan. Quy ước là sử dụng một phương thức mở rộng Add{GROUP_NAME}
duy nhất để đăng ký tất cả các dịch vụ được yêu cầu bởi một tính năng framework. Ví dụ: phương thức tiện ích mở rộng AddOptions đăng ký tất cả các dịch vụ cần thiết để sử dụng tùy chọn.
Dịch vụ do framework cung cấp
Khi sử dụng bất kỳ mẫu máy chủ hoặc trình tạo ứng dụng có sẵn nào, các giá trị mặc định sẽ được áp dụng và các dịch vụ sẽ được đăng ký theo framework. Hãy xem xét một số mẫu máy chủ và trình tạo ứng dụng phổ biến nhất:
- Host.CreateDefaultBuilder()
- Host.CreateApplicationBuilder()
- Webhost.CreateDefaultBuilder()
- WebApplication.CreateBuilder()
- WebAssemblyHostBuilder.CreateDefault
- MauiApp.CreateBuilder
Sau khi tạo trình tạo từ bất kỳ API nào trong số trên, thì trình tạo IServiceCollection
sẽ có các dịch vụ do framework định nghĩa, tùy thuộc vào cách định cấu hình máy chủ. Đối với các ứng dụng dựa trên mẫu .NET, framework có thể đăng ký hàng trăm dịch vụ.
Bảng sau liệt kê một mẫu nhỏ của các dịch vụ đã đăng ký framework này:
Loại dịch vụ | Lifetime |
---|---|
Microsoft.Extensions.DependencyInjection.IServiceScopeFactory | Singleton |
IHostApplicationLifetime | Singleton |
Microsoft.Extensions.Logging.ILogger<TCategoryName> | Singleton |
Microsoft.Extensions.Logging.ILoggerFactory | Singleton |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Singleton |
Microsoft.Extensions.Options.IConfigureOptions<TOptions> | Transient |
Microsoft.Extensions.Options.IOptions<TOptions> | Singleton |
System.Diagnostics.DiagnosticListener | Singleton |
System.Diagnostics.DiagnosticSource | Singleton |
Tuổi thọ dịch vụ
Dịch vụ có thể được đăng ký với một trong các thời gian tồn tại sau:
- Transient
- Scoped
- Singleton
Những phần sau đây mô tả từng lifetime trước đó. Lựa chọn thời hạn sử dụng phù hợp cho từng dịch vụ đã đăng ký.
Transient (Tạm thời)
Các dịch vụ lifetime transient được tạo mỗi khi chúng được yêu cầu từ vùng chứa dịch vụ. Lifetime này hoạt động tốt nhất cho các dịch vụ nhẹ, không trạng thái. Đăng ký dịch vụ tạm thời với AddTransient.
Trong các ứng dụng xử lý yêu cầu, các dịch vụ transient sẽ được xử lý khi kết thúc yêu cầu.
Scoped (Phạm vi)
Đối với các ứng dụng web, thời gian tồn tại trong phạm vi cho biết rằng các dịch vụ được tạo một lần cho mỗi yêu cầu (kết nối) của khách hàng. Đăng ký dịch vụ trong phạm vi với AddScoped.
Trong các ứng dụng xử lý yêu cầu, các dịch vụ có phạm vi sẽ được xử lý ở cuối yêu cầu.
Khi sử dụng Entity Framework Core, phương thức tiện ích mở rộng AddDbContext sẽ đăng ký DbContext
các kiểu có thời gian tồn tại trong phạm vi theo mặc định.
Ghi chú
Không giải quyết một dịch vụ có phạm vi từ một dịch vụ đơn lẻ và hãy cẩn thận không làm như vậy một cách gián tiếp, chẳng hạn như thông qua một dịch vụ tạm thời. Nó có thể khiến dịch vụ có trạng thái không chính xác khi xử lý các yêu cầu tiếp theo. Sẽ không có vấn đề gì nếu:
- Giải quyết một dịch vụ đơn lẻ từ một dịch vụ có phạm vi hoặc tạm thời.
- Giải quyết một dịch vụ có phạm vi từ một dịch vụ có phạm vi hoặc tạm thời khác.
Theo mặc định, trong môi trường phát triển, việc giải quyết một dịch vụ từ một dịch vụ khác có thời gian tồn tại lâu hơn sẽ tạo ra một ngoại lệ. Để biết thêm thông tin, hãy xem Xác thực phạm vi.
Singleton
Các dịch vụ lifetime của Singleton được tạo ra:
- Lần đầu tiên chúng được yêu cầu.
- Bởi nhà phát triển, khi cung cấp phiên bản triển khai trực tiếp vào vùng chứa. Cách tiếp cận này hiếm khi cần thiết.
Mọi yêu cầu triển khai dịch vụ tiếp theo từ vùng chứa DI đều sử dụng cùng một phiên bản. Nếu ứng dụng yêu cầu hành vi đơn lẻ, hãy cho phép vùng chứa dịch vụ quản lý vòng đời của dịch vụ. Không triển khai mẫu thiết kế singleton và cung cấp mã để loại bỏ singleton. Các dịch vụ không bao giờ được xử lý bằng mã đã giải quyết dịch vụ từ vùng chứa. Nếu một kiểu hoặc factory được đăng ký dưới dạng đơn lẻ, thùng chứa sẽ tự động loại bỏ đơn lẻ đó.
Đăng ký dịch vụ singleton với AddSingleton. Các dịch vụ Singleton phải an toàn theo luồng và thường được sử dụng trong các dịch vụ không trạng thái.
Trong các ứng dụng xử lý yêu cầu, các dịch vụ đơn lẻ sẽ được xử lý khi ServiceProvider được xử lý khi tắt ứng dụng. Vì bộ nhớ không được giải phóng cho đến khi ứng dụng tắt, nên hãy cân nhắc việc sử dụng bộ nhớ với dịch vụ đơn lẻ.
Phương thức đăng ký dịch vụ
Framework này cung cấp các phương thức gia hạn đăng ký dịch vụ hữu ích trong các tình huống cụ thể:
Phương thức | Tự động xử lý đối tượng |
Nhiều triển khai |
Truyền đối số |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() Ví dụ: services.AddSingleton<IMyDep, MyDep>(); |
Yes | Yes | No |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) Ví dụ: services.AddSingleton<IMyDep>(sp => new MyDep()); services.AddSingleton<IMyDep>(sp => new MyDep(99)); |
Yes | Yes | Yes |
Add{LIFETIME}<{IMPLEMENTATION}>() Ví dụ: services.AddSingleton<MyDep>(); |
Yes | No | No |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) Ví dụ: services.AddSingleton<IMyDep>(new MyDep()); services.AddSingleton<IMyDep>(new MyDep(99)); |
No | Yes | Yes |
AddSingleton(new {IMPLEMENTATION}) Ví dụ: services.AddSingleton(new MyDep()); services.AddSingleton(new MyDep(99)); |
No | No | Yes |
Để biết thêm thông tin về loại bỏ, hãy xem phần Loại bỏ dịch vụ.
Việc đăng ký một dịch vụ chỉ có một loại triển khai tương đương với việc đăng ký dịch vụ đó với cùng loại triển khai và dịch vụ. Đây là lý do tại sao không thể đăng ký nhiều triển khai dịch vụ bằng các phương pháp không sử dụng loại dịch vụ rõ ràng. Các phương thức này có thể đăng ký nhiều thể hiện của một dịch vụ, nhưng chúng đều có cùng kiểu triển khai.
Bất kỳ phương thức đăng ký dịch vụ nào ở trên đều có thể được sử dụng để đăng ký nhiều thể hiện dịch vụ của cùng một loại dịch vụ. Trong ví dụ sau, AddSingleton
được gọi hai lần với kiểu dịch vụ IMessageWriter
. Lệnh gọi thứ hai tới AddSingleton
sẽ ghi đè lệnh gọi trước đó khi được giải quyết dưới dạng IMessageWriter
và thêm vào lệnh gọi trước đó khi nhiều dịch vụ được giải quyết thông qua IEnumerable<IMessageWriter>
. Các dịch vụ xuất hiện theo thứ tự chúng được đăng ký khi được giải quyết qua IEnumerable<{SERVICE}>
.
using ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
builder.Services.AddSingleton<ExampleService>();
using IHost host = builder.Build();
_ = host.Services.GetService<ExampleService>();
await host.RunAsync();
Mã nguồn mẫu trên đăng ký hai triển khai của IMessageWriter
.
using System.Diagnostics;
namespace ConsoleDI.IEnumerableExample;
public sealed class ExampleService
{
public ExampleService(
IMessageWriter messageWriter,
IEnumerable<IMessageWriter> messageWriters)
{
Trace.Assert(messageWriter is LoggingMessageWriter);
var dependencyArray = messageWriters.ToArray();
Trace.Assert(dependencyArray[0] is ConsoleMessageWriter);
Trace.Assert(dependencyArray[1] is LoggingMessageWriter);
}
}
Việc ExampleService
định nghĩa hai tham số hàm tạo; một IMessageWriter
đơn và một IEnumerable<IMessageWriter>
. IMessageWriter
đơn là bản triển khai cuối cùng đã được đăng ký, trong khi bản IEnumerable<IMessageWriter>
đại diện cho tất cả các bản triển khai đã đăng ký.
Framework này cũng cung cấp các phương thức mở rộng TryAdd{LIFETIME}
, chỉ đăng ký dịch vụ nếu chưa đăng ký triển khai.
Trong ví dụ sau, lời gọi tới AddSingleton
sẽ đăng ký ConsoleMessageWriter
làm triển khai cho IMessageWriter
. Lời gọi tới TryAddSingleton
không có hiệu lực vì IMessageWriter
đã có triển khai đã đăng ký:
services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>();
TryAddSingleton
không có tác dụng vì nó đã được thêm vào và việc "try" sẽ thất bại. ExampleService
sẽ khẳng định như sau:
public class ExampleService
{
public ExampleService(
IMessageWriter messageWriter,
IEnumerable<IMessageWriter> messageWriters)
{
Trace.Assert(messageWriter is ConsoleMessageWriter);
Trace.Assert(messageWriters.Single() is ConsoleMessageWriter);
}
}
Để biết thêm thông tin, hãy xem:
Các phương thức TryAddEnumerable(ServiceDescriptor) chỉ đăng ký dịch vụ nếu chưa có triển khai cùng kiểu. Nhiều dịch vụ được giải quyết thông qua IEnumerable<{SERVICE}>
. Khi đăng ký dịch vụ, hãy thêm một thể hiện nếu một trong các loại tương tự chưa được thêm. Tác giả thư viện sử dụng TryAddEnumerable
để tránh đăng ký nhiều bản sao của quá trình triển khai trong vùng chứa.
Trong ví dụ sau, lệnh gọi đầu tiên TryAddEnumerable
đăng ký MessageWriter
làm phần triển khai cho IMessageWriter1
. Lời gọi thứ hai đăng ký MessageWriter
cho IMessageWriter2
. Lời gọi thứ ba không có hiệu lực vì IMessageWriter1
đã đăng ký triển khai MessageWriter
:
public interface IMessageWriter1 { }
public interface IMessageWriter2 { }
public class MessageWriter : IMessageWriter1, IMessageWriter2
{
}
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter2, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
Việc đăng ký dịch vụ thường không phụ thuộc vào thứ tự ngoại trừ khi đăng ký nhiều triển khai cùng kiểu.
IServiceCollection
là một tập hợp các đối tượng ServiceDescriptor. Ví dụ sau đây cho thấy cách đăng ký dịch vụ bằng cách tạo và thêm ServiceDescriptor
:
string secretKey = Configuration["SecretKey"];
var descriptor = new ServiceDescriptor(
typeof(IMessageWriter),
_ => new DefaultMessageWriter(secretKey),
ServiceLifetime.Transient);
services.Add(descriptor);
Các phương thức có sẵn Add{LIFETIME}
sử dụng cùng một cách tiếp cận. Ví dụ: xem mã nguồn AddScoped.
Hành vi chèn hàm tạo (injection constructor)
Dịch vụ có thể được giải quyết bằng cách sử dụng:
- IServiceProvider
- ActivatorUtilities:
- Tạo các đối tượng chưa được đăng ký trong vùng chứa.
- Được sử dụng với một số tính năng framework.
Hàm tạo có thể chấp nhận các đối số không được cung cấp bởi tính năng DI, nhưng các đối số phải gán các giá trị mặc định.
Khi các dịch vụ được giải quyết bằng IServiceProvider
hoặc ActivatorUtilities
, việc chèn hàm tạo sẽ yêu cầu một hàm tạo public.
Khi các dịch vụ được giải quyết bằng ActivatorUtilities
, việc chèn hàm tạo yêu cầu chỉ tồn tại một hàm tạo có thể áp dụng. Tải chồng (Overloading) hàm tạo được hỗ trợ, nhưng chỉ có thể tồn tại một tình trạng tải chồng mà tất cả các đối số của nó có thể được đáp ứng bởi DI.
Xác thực phạm vi (Scope validation)
Khi ứng dụng chạy trong môi trường Development
và gọi CreateApplicationBuilder để xây dựng máy chủ, nhà cung cấp dịch vụ mặc định sẽ thực hiện kiểm tra để xác minh rằng:
- Các dịch vụ có phạm vi không được giải quyết từ nhà cung cấp dịch vụ gốc.
- Các dịch vụ có phạm vi không được đưa vào các dịch vụ đơn lẻ.
Nhà cung cấp dịch vụ gốc được tạo khi BuildServiceProvider được gọi. Vòng đời của nhà cung cấp dịch vụ gốc tương ứng với vòng đời của ứng dụng khi nhà cung cấp khởi động với ứng dụng và bị loại bỏ khi ứng dụng tắt.
Các dịch vụ có phạm vi được xử lý bởi vùng chứa đã tạo ra chúng. Nếu một dịch vụ có phạm vi được tạo trong vùng chứa gốc thì thời gian tồn tại của dịch vụ đó sẽ được tăng lên thành singleton một cách hiệu quả vì nó chỉ được vùng chứa gốc xử lý khi ứng dụng tắt. Việc xác thực phạm vi dịch vụ sẽ nắm bắt được những tình huống này khi BuildServiceProvider
được gọi.
Kịch bản phạm vi (Scope scenarios)
IServiceScopeFactory luôn được đăng ký dưới dạng đơn lẻ, nhưng IServiceProvider có thể thay đổi tùy theo thời gian tồn tại của lớp chứa. Ví dụ: nếu bạn giải quyết các dịch vụ từ một phạm vi và bất kỳ dịch vụ nào trong số đó có IServiceProvider thì đó sẽ là một thể hiện có phạm vi.
Để đạt được các dịch vụ xác định phạm vi trong quá trình triển khai IHostedService, chẳng hạn như BackgroundService, thì đừng chèn các phần phụ thuộc dịch vụ thông qua việc chèn hàm tạo. Thay vào đó, hãy chèn IServiceScopeFactory, tạo một phạm vi, sau đó giải quyết các phần phụ thuộc từ phạm vi đó để sử dụng thời gian sử dụng dịch vụ phù hợp.
namespace WorkerScope.Example;
public sealed class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory) =>
(_logger, _serviceScopeFactory) = (logger, serviceScopeFactory);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
{
try
{
_logger.LogInformation(
"Starting scoped work, provider hash: {hash}.",
scope.ServiceProvider.GetHashCode());
var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
var next = await store.GetNextAsync();
_logger.LogInformation("{next}", next);
var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
await processor.ProcessAsync(next);
_logger.LogInformation("Processing {name}.", next.Name);
var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
await relay.RelayAsync(next);
_logger.LogInformation("Processed results have been relayed.");
var marked = await store.MarkAsync(next);
_logger.LogInformation("Marked as processed: {next}", marked);
}
finally
{
_logger.LogInformation(
"Finished scoped work, provider hash: {hash}.{nl}",
scope.ServiceProvider.GetHashCode(), Environment.NewLine);
}
}
}
}
}
Trong đoạn code trên, khi ứng dụng đang chạy, dịch vụ nền:
- Phụ thuộc vào IServiceScopeFactory.
- Tạo IServiceScope để giải quyết các dịch vụ bổ sung.
- Giải quyết các dịch vụ trong phạm vi cần thiết.
- Hoạt động trên việc xử lý các đối tượng, sau đó chuyển tiếp chúng và cuối cùng đánh dấu chúng là đã được xử lý.
Từ mã nguồn mẫu, bạn có thể thấy việc triển khai IHostedService có thể mang lại lợi ích như thế nào từ thời gian sử dụng dịch vụ có phạm vi.
Dịch vụ có khóa
.NET cũng hỗ trợ đăng ký và tra cứu dịch vụ dựa trên một khóa, nghĩa là có thể đăng ký nhiều dịch vụ bằng một khóa khác và sử dụng khóa này để tra cứu.
Ví dụ: hãy xem xét việc bạn có cách triển khai khác nhau của interface IMessageWriter
: MemoryMessageWriter
và QueueMessageWriter
.
Bạn có thể đăng ký các dịch vụ này bằng cách sử dụng quá tải các phương thức đăng ký dịch vụ (đã xem trước đó) hỗ trợ khóa làm tham số:
services.AddSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddSingleton<IMessageWriter, QueueMessageWriter>("queue");
key
là không giới hạn ở string
, nó có thể là bất kỳ object
nào mà bạn muốn, miễn là loại triển khai chính xác Equals
.
Trong hàm tạo của lớp sử dụng IMessageWriter
, bạn thêm FromKeyedServicesAttribution để chỉ định khóa của dịch vụ cần giải quyết:
public class ExampleService
{
public ExampleService(
[FromKeyedServices("queue")] IMessageWriter writer)
{
// Omitted for brevity...
}
}
Xem thêm
- Sử dụng Dependency Injection trong .NET
- Hướng dẫn Dependency Injection
- Dependency Injection trong ASP.NET Core
- Mô hình hội nghị NDC để phát triển ứng dụng DI
- Nguyên lý phụ thuộc tường minh
- Đảo ngược các vùng chứa điều khiển và mẫu DI (Martin Fowler)
- Lỗi DI phải được tạo trong repo github.com/dotnet/extensions