7.2 - IoC Container và DI Container
Tóm tắt: Ba từ hay bị dùng thay cho nhau nhưng nằm ở ba tầng khác nhau. IoC là nguyên tắc: quyền quyết định bị đảo từ code của bạn sang framework. DI là một cách hiện thực IoC, cụ thể cho việc cung cấp phụ thuộc. Container là công cụ tự động hoá DI — và bạn có thể làm DI hoàn toàn không cần container (gọi là pure DI). Về mặt kỹ thuật,
ServiceCollectionlà công thức, cònServiceProviderlà nhà bếp: đăng ký chỉ ghi lại cách tạo, chưa tạo gì cả;BuildServiceProvider()mới dựng container từ danh sách đó, và container đã dựng không thấy những đăng ký thêm vào sau.
Mục tiêu bài học
Sau bài này bạn có thể:
- Phân biệt rạch ròi IoC, DI và DI container.
- Giải thích
ServiceDescriptor,ServiceCollectionvàServiceProviderkhác nhau thế nào. - Mô tả đúng những gì xảy ra khi container resolve một service.
- Biết khi nào container .NET là đủ và khi nào cần Autofac.
Cần biết trước: bài 7.1, generic method kiểu Add<TService, TImpl>() (bài 5.2), lambda (bài 5.4).
Các ví dụ console trong module chạy được trong một project dotnet new console đã cài gói container: dotnet add package Microsoft.Extensions.DependencyInjection. Dán code vào Program.cs, giữ các khai báo class, interface ở cuối file (sau code chạy), rồi dotnet run.
Nội dung bài học
7.2.1 — Ba tầng khái niệm
IoC rộng hơn DI rất nhiều. Mỗi lần bạn viết một event handler, một middleware, hay override một phương thức của lớp cha, bạn đang dùng IoC: framework gọi bạn, chứ không phải bạn gọi framework. Đây là "nguyên tắc Hollywood" — đừng gọi cho chúng tôi, chúng tôi sẽ gọi bạn.
DI là IoC áp cho một thứ cụ thể: việc lấy phụ thuộc. Thay vì lớp tự đi tìm, phụ thuộc được đưa tới.
Container chỉ là tiện ích. DI không cần container:
// Pure DI — hoàn toàn không container
var repository = new PostgresCustomerRepository(connectionString);
var email = new SmtpEmailService(smtpHost, smtpPort);
var service = new CustomerService(repository, email, logger);Đoạn trên là DI đầy đủ, đúng nghĩa. Với 10 lớp thì nó còn dễ đọc hơn container. Với 300 lớp và bốn mức lồng nhau thì nó thành một hàm dài 500 dòng — đó chính là lúc container có giá trị.
TIP
Nhớ đơn giản
IoC là tại sao. DI là cái gì. Container là bằng gì.
7.2.2 — ServiceCollection: công thức, chưa phải món ăn
var services = new ServiceCollection();
services.AddScoped<ICustomerRepository, PostgresCustomerRepository>();
services.AddScoped<IEmailService, SmtpEmailService>();
services.AddScoped<CustomerService>();Sau ba dòng này, chưa có đối tượng nào được tạo. ServiceCollection chỉ là IList<ServiceDescriptor>, và mỗi ServiceDescriptor ghi ba thông tin:
| Thành phần | Ví dụ | Nghĩa |
|---|---|---|
ServiceType | ICustomerRepository | Khi ai đó xin kiểu này |
ImplementationType | PostgresCustomerRepository | thì đưa kiểu này |
Lifetime | Scoped | và dùng lại theo quy tắc này |
Vì nó là một IList, thứ tự có ý nghĩa: đăng ký sau ghi đè đăng ký trước khi resolve một instance. Điều này giải thích một hành vi hay gây bối rối — đăng ký IEmailService hai lần rồi GetRequiredService<IEmailService>() sẽ trả về cái cuối cùng. Nhưng GetServices<IEmailService>() thì trả về cả hai, theo đúng thứ tự đăng ký.
Muốn "chỉ đăng ký nếu chưa có ai đăng ký" thì dùng TryAdd:
services.TryAddScoped<IEmailService, SmtpEmailService>();Đây là thứ mọi thư viện nên dùng, để không đè lên lựa chọn của ứng dụng.
7.2.3 — ServiceProvider: nơi mọi thứ thật sự xảy ra
ServiceProvider provider = services.BuildServiceProvider();
var customerService = provider.GetRequiredService<CustomerService>();BuildServiceProvider() dựng một container từ danh sách đăng ký tại thời điểm gọi. Bạn vẫn thêm được đăng ký vào services sau đó, nhưng container đã dựng không thấy chúng: chạy thử trên .NET 10, provider.GetService<IClock>() vẫn trả null dù IClock được Add ngay sau khi build; gọi BuildServiceProvider() lần nữa thì được một container thứ hai, độc lập với cái đầu. Với ứng dụng web, builder.Build() còn khoá hẳn danh sách: thêm đăng ký sau đó là ném lỗi ngay (bài 7.6).
Khi bạn xin CustomerService, container làm theo thứ tự:
- Tìm
ServiceDescriptorkhớp vớiCustomerService. - Đọc constructor, thấy nó cần
ICustomerRepository,IEmailService,ILogger<CustomerService>. - Đệ quy resolve từng cái trong số đó.
- Gọi constructor với các đối tượng vừa dựng.
- Ghi lại đối tượng vào scope hiện tại nếu lifetime yêu cầu, và ghi vào danh sách cần
Disposenếu nó làIDisposable.
Điểm 5 là thứ bạn nhận được miễn phí và dễ quên: container gọi Dispose giúp bạn cho mọi đối tượng nó tạo ra. Nhưng chỉ cho những đối tượng nó tạo — nếu bạn tự new rồi đăng ký instance đó, việc dọn dẹp là của bạn.
Bước 2 có một quy tắc quan trọng: nếu lớp có nhiều constructor, container chọn cái mà nó giải được nhiều tham số nhất. Nó không chọn cái dài nhất, và nếu có hai constructor cùng "giải được" mà không cái nào bao trùm cái kia, bạn nhận exception. Cách an toàn nhất là chỉ có một constructor công khai.
GetService<T>() trả null khi không tìm thấy; GetRequiredService<T>() ném exception với thông báo rõ ràng. Luôn dùng bản Required — NullReferenceException ở đâu đó sâu trong nghiệp vụ tệ hơn nhiều so với một lỗi khởi động nói rõ bạn quên đăng ký gì.
7.2.4 — Trong ASP.NET Core bạn không thấy hai bước đó
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ICustomerRepository, PostgresCustomerRepository>();
var app = builder.Build(); // container được dựng ở đâybuilder.Services chính là một IServiceCollection, và builder.Build() gọi BuildServiceProvider() bên trong. Đó là lý do mọi đăng ký phải nằm trước builder.Build(). Chi tiết ở bài 7.6.
Khi dùng WebApplication.CreateBuilder (hoặc Host.CreateApplicationBuilder cho console), host đã đăng ký sẵn khá nhiều service. Hay dùng nhất: ILogger<T>, IConfiguration, IHostEnvironment (với web là IWebHostEnvironment), IHostApplicationLifetime, IServiceScopeFactory. Còn với new ServiceCollection() tự tạo thì container chỉ có sẵn vài service của chính nó như IServiceProvider và IServiceScopeFactory; muốn có ILogger<T> phải tự gọi services.AddLogging().
7.2.5 — Container .NET đủ chưa?
Microsoft.Extensions.DependencyInjection cố ý tối giản. Nó không có:
- Property injection — chỉ hỗ trợ constructor.
- Assembly scanning — không tự quét và đăng ký hàng loạt.
- Decorator — không bọc service này bằng service khác một cách tự nhiên.
- Conditional registration phức tạp — mặc dù .NET 8 đã có keyed services (bài 7.8).
Đó là lựa chọn thiết kế: nhanh, ít phép màu, ai đọc Program.cs cũng hiểu.
| Dùng container .NET | Cân nhắc Autofac / Scrutor |
|---|---|
| Ứng dụng web thông thường | Cần decorator (caching, logging bọc quanh repository) |
| Số lượng đăng ký vừa phải | Cần quét assembly đăng ký hàng trăm handler |
Muốn Program.cs đọc là hiểu | Cần property injection cho legacy code |
Thực tế: Scrutor là lựa chọn nhẹ nhất, vì nó thêm assembly scanning và decorator lên trên container .NET chứ không thay thế nó.
7.2.6 — Rà lại code của bạn
Danh sách rà soát container
- Mọi đăng ký đều nằm trước builder.Build().
- Không gọi BuildServiceProvider() thủ công giữa chừng trong Program.cs.
- Dùng GetRequiredService thay vì GetService ở mọi nơi.
- Mỗi lớp được tiêm chỉ có một constructor công khai.
- Thư viện dùng TryAdd để không đè lên đăng ký của ứng dụng.
- Hiểu rằng đăng ký sau thắng đăng ký trước khi resolve một instance.
- Không tự new rồi đăng ký instance nếu nó cần được Dispose.
Bài tập áp dụng
Bài 1 — Đăng ký trùng: cái nào thắng
Đăng ký IEmailService hai lần với hai cài đặt khác nhau. Gọi GetRequiredService và GetServices, ghi lại kết quả của cả hai.
Tiêu chí hoàn thành: bạn nêu đúng quy tắc và một tình huống thực tế mà quy tắc đó có ích.
Gợi ý và lời giải — Bài 1
Gợi ý. Container giữ các đăng ký trong một danh sách theo thứ tự. Hai phương thức lấy ra đọc danh sách đó theo hai cách khác nhau.
Lời giải.
// IEmail.cs
public interface IEmail { string Name { get; } }
public class SmtpEmail : IEmail { public string Name => "Smtp"; }
public class SendGridEmail : IEmail { public string Name => "SendGrid"; }// Program.cs
using Microsoft.Extensions.DependencyInjection;
var s = new ServiceCollection();
s.AddSingleton<IEmail, SmtpEmail>();
s.AddSingleton<IEmail, SendGridEmail>();
var p = s.BuildServiceProvider();
Console.WriteLine($"GetRequiredService -> {p.GetRequiredService<IEmail>().Name}");
Console.WriteLine($"GetServices -> {string.Join(", ", p.GetServices<IEmail>().Select(x => x.Name))}");Dán vào một file Program.cs thì khối Program.cs đặt lên đầu, khối khai báo đặt xuống cuối (C# bắt buộc, nếu không sẽ báo lỗi CS8803).
Kết quả chạy trên .NET 10:
GetRequiredService -> SendGrid
GetServices -> Smtp, SendGridQuy tắc: đăng ký cuối cùng thắng. GetRequiredService trả về cái được đăng ký sau cùng. GetServices trả về tất cả, theo đúng thứ tự đăng ký.
Tình huống thực tế mà quy tắc này có ích — ghi đè mặc định:
builder.Services.AddCrmInfrastructure(builder.Configuration); // đăng ký SmtpEmailSender
if (builder.Environment.IsDevelopment())
builder.Services.AddSingleton<IEmailSender, FileEmailSender>(); // ghi ra file thay vì gửi thậtỞ môi trường phát triển, FileEmailSender được đăng ký sau nên nó thắng — mà không phải sửa gì trong AddCrmInfrastructure. Đây là cách phổ biến để thay thế hạ tầng thật bằng bản giả trong lúc phát triển và khi chạy kiểm thử tích hợp.
Cùng cơ chế được WebApplicationFactory (công cụ chạy cả ứng dụng web trong kiểm thử tích hợp, bài 9.8) dùng để thay service trong kiểm thử:
builder.ConfigureServices(services =>
{
services.AddSingleton<IEmailSender, FakeEmailSender>(); // ghi đè bản thật
});Cái bẫy của quy tắc này. Nó cũng có nghĩa là đăng ký trùng do vô ý sẽ im lặng thay đổi hành vi. Đăng ký thừa vẫn nằm trong container, vẫn được tạo ra nếu ai đó gọi GetServices, và vẫn tốn bộ nhớ. Đây chính là vấn đề mà bài 7.5 xử lý bằng TryAdd.
Ba cách lấy nhiều cài đặt cùng lúc:
// 1. GetServices — lấy tất cả
var allSenders = sp.GetServices<IEmailSender>();
// 2. Tiêm IEnumerable<T> vào constructor — cách thường dùng nhất
public class NotificationHub(IEnumerable<INotificationSender> senders) { }
// 3. Keyed services (.NET 8) — chọn theo khoá
builder.Services.AddKeyedScoped<INotificationSender, EmailSender>("email");
builder.Services.AddKeyedScoped<INotificationSender, SmsSender>("sms");
public class Svc([FromKeyedServices("email")] INotificationSender sender) { }Cách 2 là mẫu chuẩn khi bạn muốn chạy tất cả — ví dụ chạy mọi validator, gửi qua mọi kênh. Cách 3 dành cho khi bạn muốn chọn một, và bài 7.8 trình bày chi tiết.
Một chi tiết về thứ tự. GetServices giữ nguyên thứ tự đăng ký, và điều đó có ý nghĩa với những thứ chạy tuần tự như bộ lọc hay đường ống xử lý. Nếu thứ tự quan trọng, hãy để nó rõ ràng trong code đăng ký thay vì trông chờ vào may mắn.
Bài 2 — Container tự dọn dẹp tới đâu
Tạo một service cài IDisposable ghi log trong Dispose. Đăng ký Scoped, tạo một phạm vi rồi huỷ. Xác nhận log xuất hiện. Sau đó thử với AddSingleton(new MyService()) và so sánh.
Tiêu chí hoàn thành: bạn nêu đúng quy tắc — container chỉ giải phóng thứ chính nó tạo ra.
Gợi ý và lời giải — Bài 2
Gợi ý. Có hai cách đăng ký một Singleton: đưa kiểu cho container tự tạo, hoặc đưa một thể hiện đã có sẵn. Ai là người "sở hữu" đối tượng trong mỗi trường hợp? Dùng ba kiểu khác nhau cho ba cách đăng ký: nếu cả ba cùng một kiểu thì đăng ký sau ghi đè đăng ký trước (Bài 1) và bạn không quan sát được gì.
Lời giải.
// Services.cs
public class ScopedSvc : IDisposable { public void Dispose() => Console.WriteLine("Dispose ScopedSvc"); }
public class InstanceSvc : IDisposable { public void Dispose() => Console.WriteLine("Dispose InstanceSvc"); }
public class FactorySvc : IDisposable { public void Dispose() => Console.WriteLine("Dispose FactorySvc (singleton do container tạo)"); }// Program.cs
using Microsoft.Extensions.DependencyInjection;
var s = new ServiceCollection();
s.AddScoped(_ => new ScopedSvc()); // container tạo qua factory
s.AddSingleton(new InstanceSvc()); // BẠN tạo, container chỉ giữ hộ
s.AddSingleton<FactorySvc>(); // container tạo
var p = s.BuildServiceProvider();
p.GetRequiredService<FactorySvc>();
using (var scope = p.CreateScope())
{
scope.ServiceProvider.GetRequiredService<ScopedSvc>();
scope.ServiceProvider.GetRequiredService<InstanceSvc>();
}
Console.WriteLine("-- đã huỷ scope --");
p.Dispose();
Console.WriteLine("-- đã huỷ provider --");Kết quả chạy trên .NET 10:
Dispose ScopedSvc
-- đã huỷ scope --
Dispose FactorySvc (singleton do container tạo)
-- đã huỷ provider --Không có dòng Dispose InstanceSvc. Thể hiện đăng ký bằng AddSingleton(new InstanceSvc()) không bao giờ được Dispose, kể cả khi provider bị huỷ.
Quy tắc: container chỉ giải phóng thứ chính nó tạo ra. Khi bạn đưa vào một thể hiện đã dựng sẵn, container coi đó là đối tượng của bạn và không đụng tới vòng đời của nó — vì nó không biết bạn có còn dùng nó ở nơi khác hay không.
Bảng đầy đủ:
| Cách đăng ký | Ai tạo | Container Dispose? |
|---|---|---|
AddScoped<IFoo, Foo>() | Container | Có, khi phạm vi kết thúc |
AddScoped(sp => new Foo()) | Container (qua factory) | Có |
AddSingleton<IFoo, Foo>() | Container | Có, khi ứng dụng tắt |
AddSingleton(new Foo()) | Bạn | Không |
AddSingleton<IFoo>(sp => new Foo()) | Container | Có |
Lưu ý dòng thứ tư khác hẳn dòng thứ năm, dù trông rất giống nhau: một bên truyền đối tượng, một bên truyền hàm tạo đối tượng.
Hệ quả thực tế. Với đối tượng nắm tài nguyên hệ thống — kết nối, file, socket — việc quên giải phóng gây rò rỉ thật:
// RÒ RỈ — data source (giữ pool connection) không bao giờ được Dispose khi ứng dụng tắt
builder.Services.AddSingleton(NpgsqlDataSource.Create(cs));
// ĐÚNG — container tạo qua factory nên container Dispose khi tắt
builder.Services.AddSingleton(sp => NpgsqlDataSource.Create(cs));Với ứng dụng web chạy liên tục thì hậu quả nhẹ, vì tiến trình tắt là hệ điều hành thu hồi hết. Nhưng với ứng dụng chạy rồi thoát — công cụ dòng lệnh, tác vụ theo lịch — hoặc khi kiểm thử tạo và huỷ container nhiều lần, rò rỉ này tích tụ thật.
Còn IAsyncDisposable? Container hỗ trợ cả hai. Nhưng nếu bạn huỷ provider đồng bộ trong khi service cài IAsyncDisposable, bạn nhận ngoại lệ:
InvalidOperationException: 'AsyncOnly' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.Trong ASP.NET Core, host tự gọi DisposeAsync nên không sao. Khi tự tạo provider, hãy dùng await using:
await using var p = s.BuildServiceProvider();Quy tắc thực dụng. Ưu tiên AddSingleton<TService, TImpl>() hoặc factory, tránh AddSingleton(instance). Chỉ dùng cách truyền thể hiện cho những đối tượng không nắm tài nguyên — cấu hình đã đọc xong, bảng tra hằng số, đối tượng bất biến.
Bài 3 — Container chọn constructor nào
Viết một lớp có hai constructor, một nhận IEmail và một nhận IEmail cùng IScopedBar, trong đó chỉ đăng ký IEmail. Đoán xem container chọn cái nào, rồi kiểm chứng.
Tiêu chí hoàn thành: bạn nêu đúng quy tắc chọn và một trường hợp mà quy tắc đó gây bất ngờ.
Gợi ý và lời giải — Bài 3
Gợi ý. Container muốn dùng constructor nhiều tham số nhất, nhưng nó chỉ dùng được những constructor mà nó thoả mãn được toàn bộ tham số.
Lời giải.
// TwoCtors.cs
public interface IEmail { }
public class SmtpEmail : IEmail { }
public interface IScopedBar { }
public class ScopedBar : IScopedBar { }
public class TwoCtors
{
public string Used { get; }
public TwoCtors(IEmail e) => Used = "constructor 1 tham số";
public TwoCtors(IEmail e, IScopedBar b) => Used = "constructor 2 tham số";
}// Program.cs
using Microsoft.Extensions.DependencyInjection;
var s = new ServiceCollection();
s.AddSingleton<IEmail, SmtpEmail>(); // chỉ đăng ký IEmail
s.AddTransient<TwoCtors>();
Console.WriteLine($"Chỉ đăng ký IEmail -> {s.BuildServiceProvider().GetRequiredService<TwoCtors>().Used}");Kết quả chạy trên .NET 10:
Chỉ đăng ký IEmail -> constructor 1 tham sốQuy tắc. Container chọn constructor nhiều tham số nhất mà nó thoả mãn được toàn bộ. Ở đây nó không có IScopedBar, nên nó lùi về constructor một tham số.
Trường hợp gây bất ngờ — và đây là phần đáng giá của bài. Giả sử bạn thêm một tính năng mới và đăng ký IScopedBar (thêm hai dòng này vào cuối khối Program.cs):
s.AddScoped<IScopedBar, ScopedBar>(); // thêm cho một tính năng khác
Console.WriteLine($"Đăng ký thêm IScopedBar -> {s.BuildServiceProvider().GetRequiredService<TwoCtors>().Used}");Đăng ký thêm IScopedBar -> constructor 2 tham sốContainer âm thầm chuyển sang constructor hai tham số. Hành vi của TwoCtors thay đổi hoàn toàn, mà:
- Không một dòng nào trong
TwoCtorsbị sửa. - Không một cảnh báo nào.
- Người thêm dòng
AddScopedkhông hề biết mình vừa đổi hành vi của một lớp khác.
Đây là một dạng phụ thuộc ẩn giữa hai phần tưởng như không liên quan.
Khi nào container ném lỗi:
public class Ambiguous
{
public Ambiguous(IEmail e, IScopedBar b) { }
public Ambiguous(IEmail e, IOther o) { }
}Nếu container thoả mãn được cả hai (đã đăng ký cả IScopedBar lẫn IOther) và chúng cùng số tham số, nó không biết chọn cái nào. Chạy trên .NET 10:
InvalidOperationException: Unable to activate type 'Ambiguous'. The following constructors are ambiguous:
Void .ctor(IEmail, IScopedBar)
Void .ctor(IEmail, IOther)Khuyến nghị: mỗi lớp chỉ nên có MỘT constructor công khai. Ba lý do:
- Không có gì để container đoán. Hành vi tất định, không phụ thuộc vào đăng ký ở nơi khác.
- Chữ ký là tài liệu chính xác. Đọc constructor là biết đúng lớp này cần gì, không phải "có thể cần gì tuỳ tình huống".
- Kiểm thử rõ ràng. Không phải tự hỏi bài kiểm thử đang dùng constructor nào.
Với C# 12, primary constructor làm điều này thành mặc định tự nhiên:
public class OrderService(
IOrderRepository repo,
IEmailSender mail,
ILogger<OrderService> log)
{
// chỉ có một constructor, không có gì để đoán
}Khi thật sự cần nhiều cách khởi tạo, đừng dùng nhiều constructor công khai. Dùng phương thức nhà máy tĩnh với tên mô tả rõ:
public static OrderService ForTesting(IOrderRepository repo)
=> new(repo, new NullEmailSender(), NullLogger<OrderService>.Instance);Cách này vừa rõ ý định vừa không đưa sự mơ hồ vào container.
Tự kiểm tra
IoC, DI và DI container khác nhau thế nào?
IoC là nguyên tắc: quyền điều khiển bị đảo, framework gọi code của bạn thay vì ngược lại. DI là một cách hiện thực IoC, áp riêng cho việc cung cấp phụ thuộc từ bên ngoài. Container là công cụ tự động hoá DI. Bạn hoàn toàn có thể làm DI mà không cần container, gọi là pure DI, và với dự án nhỏ thì nó còn dễ đọc hơn.
ServiceCollection và ServiceProvider khác nhau ra sao?
ServiceCollection chỉ là một danh sách ServiceDescriptor ghi lại cách tạo service, chưa tạo đối tượng nào. ServiceProvider là container thật, được dựng bằng BuildServiceProvider từ danh sách tại thời điểm gọi. Thêm đăng ký vào danh sách sau thời điểm đó thì container đã dựng không thấy; còn với ứng dụng web, builder.Build() khoá hẳn danh sách và thêm vào là ném lỗi.
Đăng ký cùng một interface hai lần thì sao?
GetRequiredService trả về đăng ký cuối cùng, vì đăng ký sau ghi đè đăng ký trước. Nhưng GetServices trả về cả hai theo đúng thứ tự đăng ký. Thư viện nên dùng TryAdd để không đè lên lựa chọn của ứng dụng.
Container chọn constructor nào khi lớp có nhiều constructor?
Nó chọn constructor mà nó giải được nhiều tham số nhất, không phải cái dài nhất. Nếu có hai constructor cùng giải được mà không cái nào bao trùm cái kia thì bạn nhận exception. Cách an toàn nhất là mỗi lớp chỉ có một constructor công khai.
Nên dùng GetService hay GetRequiredService?
Luôn dùng GetRequiredService. GetService trả null khi không tìm thấy, dẫn tới NullReferenceException ở đâu đó sâu trong nghiệp vụ. GetRequiredService ném exception ngay với thông báo rõ ràng về kiểu bị thiếu.
Container .NET thiếu gì so với Autofac?
Nó không có property injection, không có assembly scanning, không hỗ trợ decorator, và conditional registration hạn chế dù .NET 8 đã thêm keyed services. Đó là lựa chọn thiết kế có chủ ý để nhanh và ít phép màu. Nếu cần thêm thì Scrutor là lựa chọn nhẹ nhất vì nó bổ sung lên trên container .NET chứ không thay thế.
Kết luận
Ba điều đáng nhớ nhất:
- IoC là nguyên tắc, DI là cách làm, container là công cụ — và DI không bắt buộc phải có container.
- Đăng ký không tạo gì cả.
ServiceCollectionlà danh sách công thức;BuildServiceProvider()mới dựng container từ danh sách đó. - Container tự gọi
Disposecho mọi đối tượng nó tạo — nhưng chỉ cho những đối tượng nó tạo.
Tham khảo
- Dependency injection in .NET
- Use dependency injection in .NET
- ServiceDescriptor class
- Dependency injection guidelines
Điều hướng
- Bài trước: 7.1 — Vấn đề DI giải quyết
- Bài tiếp theo: 7.3 — Ba cách tiêm dependency
- Về module: Trang mục lục