Skip to content

7.5 - Registering Services ​

Tóm tắt: Sáu cách đăng ký, mỗi cách cho một tình huống. Cách cơ bản (AddScoped<IFoo, Foo>()) phủ 90% trường hợp; phần còn lại là nơi mọi người vấp. Factory registration cho khởi tạo cần logic — và ở đây có bẫy: factory của một Singleton nhận root provider, nên resolve một service Scoped bên trong nó là captive dependency. ValidateOnBuild không nhìn được vào trong lambda nên không bắt được lúc khởi động; chỉ ValidateScopes báo lỗi khi service được resolve lần đầu, còn khi tắt kiểm tra thì service Scoped bị giam vĩnh viễn. Open generic cho phép một dòng đăng ký phục vụ mọi IRepository<T>. Và nhiều implementation cùng interface là tính năng, không phải lỗi: container sẽ tiêm cả tập vào IEnumerable<IFoo>.

Mục tiêu bài học ​

Sau bài này bạn có thể:

  • Chọn đúng dạng đăng ký cho từng tình huống khởi tạo.
  • Viết factory registration mà không tạo captive dependency ẩn.
  • Đăng ký open generic và biết giới hạn của nó.
  • Tiêm một tập implementation qua IEnumerable<T>.
  • Viết decorator bằng container .NET thuần.

Cần biết trước: bài 7.2, bài 7.4 (lifetime, captive dependency, ValidateScopes/ValidateOnBuild), generic (bài 5.2), lambda (bài 5.4).

Nội dung bài học ​

7.5.1 — Đăng ký cơ bản ​

csharp
// Interface + implementation — 90% trường hợp
services.AddScoped<ICustomerRepository, EfCustomerRepository>();
services.AddSingleton<IEmailService, SmtpEmailService>();
services.AddTransient<ILeadScorer, RuleBasedLeadScorer>();

// Chỉ lớp cụ thể, không interface
services.AddScoped<CustomerService>();

// Instance có sẵn — CHÚ Ý: container KHÔNG Dispose cái này
services.AddSingleton(TimeProvider.System);

Dòng cuối đăng ký đồng hồ hệ thống TimeProvider.System (bài 7.1), một instance có sẵn. Ở đây có một điểm dễ bỏ sót: khi bạn đưa vào một instance đã tạo, container không coi mình là chủ sở hữu, nên nó không gọi Dispose (bài 7.2). TimeProvider.System không giữ tài nguyên nên không sao; với đối tượng giữ tài nguyên, dùng dạng factory thay thế.

AddScoped<CustomerService>() không cần interface — container dựng được mọi lớp cụ thể miễn là giải được constructor. Đừng tạo interface chỉ để có interface: nếu một interface chỉ có đúng một implementation và bạn không mock nó trong test, nó chỉ là một lớp gián tiếp thừa.

7.5.2 — Factory registration và cái bẫy trong đó ​

csharp
services.AddSingleton<IEmailService>(sp =>
{
    var settings = sp.GetRequiredService<IOptions<EmailSettings>>().Value;
    return new SmtpEmailService(settings.Host, settings.Port, settings.UseSsl);
});

Dùng khi việc dựng cần logic: đọc cấu hình, chọn implementation theo môi trường, hoặc gọi một factory của thư viện bên thứ ba. (IOptions<EmailSettings> là cách đọc một nhóm cấu hình thành một lớp có kiểu, bài 7.7 dạy kỹ.)

Cái bẫy:

csharp
services.AddSingleton<IReportService>(sp =>
{
    var repo = sp.GetRequiredService<ICustomerRepository>();   // Scoped!
    return new ReportService(repo);                            // CAPTIVE
});

Factory của một Singleton được gọi với root provider, kể cả khi lần xin đầu tiên đến từ bên trong một request. Bài 1 kiểm chứng điều này trên .NET 10:

  • Bật ValidateScopes (mặc định ở Development): lần resolve đầu tiên ném InvalidOperationException: Cannot resolve scoped service 'ICustomerRepository' from root provider.
  • Tắt kiểm tra (thường là Production): factory lấy được một ICustomerRepository từ root, khác hẳn instance của request, không bao giờ bị Dispose, và bị giam trong ReportService vĩnh viễn (bài 7.4).

Nguy hiểm hơn constructor injection ở chỗ: khi bật cả ValidateScopes và ValidateOnBuild, captive dependency qua constructor bị bắt ngay lúc khởi động, nhưng không bắt được qua factory, vì nó không nhìn vào bên trong lambda. Lỗi chỉ lộ ra khi có người resolve service đó lần đầu.

csharp
// ĐÚNG — tiêm scope factory, tạo scope khi dùng
services.AddSingleton<IReportService>(sp =>
    new ReportService(sp.GetRequiredService<IServiceScopeFactory>()));

Quy tắc: trong factory của một Singleton, chỉ resolve Singleton.

7.5.3 — Open generic ​

csharp
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));

Một dòng phục vụ mọi IRepository<Customer>, IRepository<Lead>, IRepository<Order>… Container ghép kiểu tại thời điểm resolve. Đây là cách ILogger<T> hoạt động.

Đăng ký cụ thể thắng open generic cho đúng kiểu đó:

csharp
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddScoped<IRepository<Customer>, CachedCustomerRepository>();  // riêng Customer

Giới hạn về cú pháp: không có dạng AddScoped<IRepository<>, EfRepository<>>(), phải viết typeof.

7.5.4 — Nhiều implementation cho một interface ​

Đây là tính năng, không phải xung đột:

csharp
services.AddScoped<IValidator<Customer>, EmailValidator>();
services.AddScoped<IValidator<Customer>, PhoneValidator>();
services.AddScoped<IValidator<Customer>, TaxCodeValidator>();
csharp
public class CustomerService(IEnumerable<IValidator<Customer>> validators)
{
    public async Task<Customer> CreateAsync(Customer customer, CancellationToken ct)
    {
        foreach (var v in validators)          // cả ba, đúng thứ tự đăng ký
            await v.ValidateAsync(customer, ct);
        ...
    }
}

Thêm một luật kiểm tra mới = thêm một dòng đăng ký, không sửa CustomerService. Đây chính là Open/Closed trong thực tế.

Nhớ hai điều từ bài 7.2: GetRequiredService<IValidator<Customer>>() chỉ trả về cái cuối cùng, còn IEnumerable<> trả về tất cả theo thứ tự đăng ký.

7.5.5 — TryAdd và Replace ​

csharp
services.TryAddScoped<IEmailService, SmtpEmailService>();            // chỉ thêm nếu chưa có
services.TryAddEnumerable(
    ServiceDescriptor.Scoped<IValidator<Customer>, EmailValidator>()); // không thêm trùng

services.Replace(ServiceDescriptor.Scoped<IEmailService, FakeEmailService>()); // thay the
  • TryAdd: thư viện nên luôn dùng, để ứng dụng đè lên được lựa chọn mặc định.
  • TryAddEnumerable: khi một AddXxx() bị gọi hai lần, Add thường sẽ nhân đôi và validator chạy hai lần. TryAddEnumerable so cả ServiceType lẫn ImplementationType nên không trùng.
  • Replace: rất hữu ích trong integration test để thay hạ tầng thật bằng fake.

7.5.6 — Decorator bằng container .NET thuần ​

Container .NET không có decorator sẵn, nhưng làm được bằng factory:

csharp
services.AddScoped<EfCustomerRepository>();                 // lop that

services.AddScoped<ICustomerRepository>(sp =>
    new CachedCustomerRepository(
        sp.GetRequiredService<EfCustomerRepository>(),      // inner
        sp.GetRequiredService<IMemoryCache>()));

Lớp thật được đăng ký bằng kiểu cụ thể, còn interface trỏ tới lớp bọc. CachedCustomerRepository cài ICustomerRepository và nhận EfCustomerRepository làm inner. IMemoryCache là cache trong bộ nhớ có sẵn của .NET, đăng ký bằng services.AddMemoryCache().

Cách này đọc được nhưng dài dòng khi bạn có nhiều lớp cần bọc. Khi đó thư viện Scrutor (gói NuGet Scrutor, giấy phép MIT) cho bạn một dòng:

csharp
services.AddScoped<ICustomerRepository, EfCustomerRepository>();
services.Decorate<ICustomerRepository, CachedCustomerRepository>();

7.5.7 — Tóm tắt ​

DạngKhi dùng
Add<IFoo, Foo>()Mặc định
Add<Foo>()Lớp cụ thể không cần interface
Add<IFoo>(sp => ...)Khởi tạo cần logic — trong factory Singleton chỉ resolve Singleton
Add(typeof(IFoo<>), typeof(Foo<>))Open generic
Đăng ký nhiều lầnTiêm cả tập qua IEnumerable<IFoo>
TryAdd / TryAddEnumerableTrong thư viện, để không đè lựa chọn của ứng dụng
ReplaceIntegration test

7.5.8 — Rà lại code của bạn ​

Danh sách rà soát đăng ký service

  • Không factory Singleton nào resolve service Scoped từ sp.
  • Không có interface nào chỉ tồn tại để có interface.
  • Instance tự new và đăng ký không phải là thứ cần Dispose.
  • Open generic đăng ký một lần, không lặp cho từng kiểu cụ thể.
  • Nhóm luật kiểm tra hay handler được tiêm qua IEnumerable, không switch-case.
  • Thư viện nội bộ dùng TryAdd thay vì Add.
  • Extension method AddXxx() an toàn khi bị gọi hai lần.
  • Integration test dùng Replace thay vì đăng ký chồng lên.

Bài tập áp dụng ​

Bài 1 — Ranh giới của ValidateOnBuild và ValidateScopes ​

Viết hai cách đăng ký cùng một captive dependency — một bằng constructor injection, một bằng factory — rồi bật cả ValidateScopes lẫn ValidateOnBuild. So sánh thời điểm lỗi xuất hiện.

Tiêu chí hoàn thành: bạn nêu đúng cách nào bị bắt lúc khởi động và cách nào chỉ lộ ra lúc chạy.

Gợi ý và lời giải — Bài 1

Gợi ý. ValidateOnBuild phân tích kiểu của các đăng ký để suy ra đồ thị phụ thuộc. Với một factory là lambda, nó thấy được gì bên trong lambda đó?

Lời giải.

csharp
// Foo.cs
public interface IFoo { }
public interface IScopedBar { }
public class ScopedBar : IScopedBar, IDisposable { public bool Disposed; public void Dispose() => Disposed = true; }
public class Foo(IScopedBar bar) : IFoo { public IScopedBar Bar => bar; }
csharp
// Program.cs
using Microsoft.Extensions.DependencyInjection;

static void Try(string name, Action<IServiceCollection> register)
{
    Console.WriteLine($"--- {name}");
    var s = new ServiceCollection();
    s.AddScoped<IScopedBar, ScopedBar>();
    register(s);
    try
    {
        var p = s.BuildServiceProvider(
            new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
        Console.WriteLine("    Build   : không báo lỗi");
        using var scope = p.CreateScope();
        try { scope.ServiceProvider.GetRequiredService<IFoo>(); Console.WriteLine("    Resolve : KHÔNG báo lỗi"); }
        catch (Exception e) { Console.WriteLine($"    Resolve : {e.GetType().Name}: {e.Message}"); }
    }
    catch (Exception e) { Console.WriteLine($"    Build   : {e.GetType().Name}: {e.InnerException?.Message}"); }
}

Try("constructor injection  AddSingleton<IFoo, Foo>()",
    s => s.AddSingleton<IFoo, Foo>());
Try("factory  AddSingleton<IFoo>(sp => new Foo(sp.GetRequiredService<IScopedBar>()))",
    s => s.AddSingleton<IFoo>(sp => new Foo(sp.GetRequiredService<IScopedBar>())));

// Tắt kiểm tra: factory của Singleton lấy Scoped từ đâu?
var s2 = new ServiceCollection();
s2.AddScoped<IScopedBar, ScopedBar>();
s2.AddSingleton<IFoo>(sp => new Foo(sp.GetRequiredService<IScopedBar>()));
var p2 = s2.BuildServiceProvider();
IScopedBar barInFoo;
using (var scope = p2.CreateScope())
{
    var foo = (Foo)scope.ServiceProvider.GetRequiredService<IFoo>();
    var barOfScope = scope.ServiceProvider.GetRequiredService<IScopedBar>();
    barInFoo = foo.Bar;
    Console.WriteLine($"Bar trong Foo có phải Bar của scope? {ReferenceEquals(foo.Bar, barOfScope)}");
}
Console.WriteLine($"Scope đã huỷ. Bar trong Foo đã bị Dispose? {((ScopedBar)barInFoo).Disposed}");

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:

text
--- constructor injection  AddSingleton<IFoo, Foo>()
    Build   : AggregateException: Error while validating the service descriptor 'ServiceType: IFoo Lifetime: Singleton ImplementationType: Foo': Cannot consume scoped service 'IScopedBar' from singleton 'IFoo'.
--- factory  AddSingleton<IFoo>(sp => new Foo(sp.GetRequiredService<IScopedBar>()))
    Build   : không báo lỗi
    Resolve : InvalidOperationException: Cannot resolve scoped service 'IScopedBar' from root provider.
Bar trong Foo có phải Bar của scope? False
Scope đã huỷ. Bar trong Foo đã bị Dispose? False

Hai dòng cuối là phần đáng sợ: khi tắt kiểm tra, dù IFoo được xin từ bên trong một scope, factory vẫn lấy IScopedBar từ root. Foo giữ một bản ScopedBar không thuộc request nào và không bao giờ được dọn.

Kết luận chính xác:

Cách đăng ký (bật cả hai kiểm tra)Bị bắt lúc khởi động?Bị bắt lúc resolve?
Constructor injectionCóCó
Factory (lambda)KhôngCó, nhờ ValidateScopes

Lưu ý: captive dependency qua constructor chỉ bị bắt lúc khởi động khi bật cả ValidateOnBuild và ValidateScopes. Chỉ bật ValidateOnBuild thì nó chỉ kiểm tra thiếu đăng ký, không kiểm tra luật lifetime (bài 7.11, Bài 2 đo điều này).

Vì sao khác nhau. ValidateOnBuild đọc kiểu của lớp cài đặt và dò constructor của nó để dựng đồ thị phụ thuộc. Với AddSingleton<IFoo, Foo>(), nó thấy Foo cần IScopedBar, và vì ValidateScopes cũng bật nên luật lifetime được kiểm tra ngay trên đồ thị đó.

Với factory, thứ được đăng ký chỉ là một delegate. Container không thể nhìn vào thân lambda để biết nó sẽ yêu cầu những gì — điều đó chỉ lộ ra khi lambda thật sự chạy.

Ý nghĩa với code review và vận hành:

  • Constructor injection an toàn hơn không chỉ vì rõ ràng hơn, mà vì nó kiểm chứng được tự động. Đây là một lý do nữa để ưu tiên nó, bên cạnh các lý do ở bài 7.3.
  • Factory là điểm mù lúc khởi động. Ứng dụng khởi động bình thường. Với ValidateScopes bật, lỗi chỉ xuất hiện khi có request đầu tiên chạm tới service đó, và nếu đó là một tính năng ít dùng thì có thể mất nhiều ngày. Với ValidateScopes tắt, không có lỗi nào cả, chỉ có một service Scoped bị giam.
  • Vì vậy factory phải được rà bằng mắt. Mỗi sp.GetRequiredService<T>() bên trong một đăng ký AddSingleton là một chỗ cần kiểm tra vòng đời của T.

Khi buộc phải dùng factory cho Singleton, đừng lấy service Scoped ra rồi giữ. Lấy IServiceScopeFactory và tạo phạm vi khi cần:

csharp
// Foo.cs — nhận IServiceScopeFactory thay vì IScopedBar
public class Foo(IServiceScopeFactory scopeFactory) : IFoo
{
    public async Task DoWorkAsync(CancellationToken ct)
    {
        using var scope = scopeFactory.CreateScope();
        var bar = scope.ServiceProvider.GetRequiredService<IScopedBar>();
        // dùng bar trong phạm vi này, không giữ lại
    }
}
csharp
// Program.cs
// SAI
builder.Services.AddSingleton<IFoo>(sp => new Foo(sp.GetRequiredService<IScopedBar>()));

// ĐÚNG
builder.Services.AddSingleton<IFoo>(sp => new Foo(sp.GetRequiredService<IServiceScopeFactory>()));

Khi nào factory là lựa chọn đúng. Khi việc khởi tạo cần logic mà constructor injection không diễn đạt được — đọc cấu hình, chọn cài đặt theo điều kiện, hoặc gọi một hàm khởi tạo bất đồng bộ. Ngoài ra thì đăng ký theo kiểu vừa gọn hơn vừa được kiểm chứng.

Bài 2 — Đăng ký nhân đôi ​

Viết một extension AddOrderFeature() dùng Add cho một validator, gọi nó hai lần, và đếm số lần validator chạy. Sửa bằng TryAddEnumerable và đếm lại.

Tiêu chí hoàn thành: bạn phân biệt được Add, TryAdd và TryAddEnumerable, và nêu được khi nào dùng cái nào.

Gợi ý và lời giải — Bài 2

Gợi ý. Ba phương thức này xử lý trường hợp "đã có đăng ký cho kiểu này" theo ba cách hoàn toàn khác nhau. Hãy thử từng cái và quan sát GetServices<T>() trả về gì.

Lời giải — bản có vấn đề:

csharp
public static class OrderFeatureExtensions
{
    public static IServiceCollection AddOrderFeature(this IServiceCollection s)
    {
        s.AddScoped<IValidator<Order>, OrderValidator>();     // dùng Add
        return s;
    }
}

// Program.cs — vô tình gọi hai lần
builder.Services.AddOrderFeature();
builder.Services.AddOrderFeature();     // ví dụ: một lần trong AddCrmCore, một lần trực tiếp

Validator chạy hai lần cho mỗi đơn hàng. Với validator thì hậu quả là thông báo lỗi bị lặp. Với một trình xử lý sự kiện hoặc một bộ lọc thì hậu quả nghiêm trọng hơn nhiều — email gửi hai lần, bản ghi tạo hai lần.

Ba phương thức và hành vi:

csharp
// Add — luôn thêm, dù đã có
s.AddOrderFeature(); s.AddOrderFeature();

// TryAdd — chỉ thêm nếu CHƯA CÓ đăng ký nào cho kiểu dịch vụ này
s.TryAddScoped<IValidator<Order>, OrderValidator>();
s.TryAddScoped<IValidator<Order>, OrderValidator>();
s.TryAddScoped<IValidator<Order>, EmailValidator>();

// TryAddEnumerable — chỉ thêm nếu chưa có đăng ký nào với CÙNG KIỂU CÀI ĐẶT
s.TryAddEnumerable(ServiceDescriptor.Scoped<IValidator<Order>, OrderValidator>());
s.TryAddEnumerable(ServiceDescriptor.Scoped<IValidator<Order>, OrderValidator>());
s.TryAddEnumerable(ServiceDescriptor.Scoped<IValidator<Order>, EmailValidator>());

Mỗi nhóm ba dòng chạy trên một ServiceCollection riêng, rồi in GetServices<IValidator<Order>>() trong một scope. TryAdd... nằm trong namespace Microsoft.Extensions.DependencyInjection.Extensions. Kết quả chạy trên .NET 10:

text
Add x2                                : [OrderValidator, OrderValidator]
TryAdd x2 + EmailValidator            : [OrderValidator]
TryAddEnumerable x2 + EmailValidator  : [OrderValidator, EmailValidator]

Khác biệt then chốt giữa TryAdd và TryAddEnumerable:

TryAddTryAddEnumerable
So sánh theoKiểu dịch vụKiểu dịch vụ + kiểu cài đặt
Thêm cài đặt thứ haiBị bỏ quaĐược thêm
Dùng choChỉ có một cài đặt đúngNhiều cài đặt cùng tồn tại

Đây là điểm dễ sai nhất: dùng TryAdd cho validator sẽ khiến validator thứ hai — cho một kiểu khác — bị âm thầm bỏ qua.

Bảng chọn:

Tình huốngDùng
Cho phép người dùng thư viện ghi đè mặc định của bạnTryAdd
Nhiều cài đặt cùng chạy: validator, handler, bộ lọcTryAddEnumerable
Cố ý muốn thay thế đăng ký trướcReplace
Code của chính ứng dụng, hoặc cố ý cho phép nhiều bản cùng chạy qua IEnumerable<T>Add (không bao giờ báo lỗi khi trùng, chỉ thêm một dòng nữa)

Quy tắc cho tác giả thư viện. Mọi extension AddXxx công khai nên dùng TryAdd hoặc TryAddEnumerable, không dùng Add. Lý do: bạn không kiểm soát được người dùng gọi nó bao nhiêu lần, và một extension gọi hai lần mà gây lỗi là một API dễ dùng sai.

Đây cũng chính là cách mọi gói của Microsoft được viết — AddLogging, AddOptions, AddHttpClient đều gọi được nhiều lần mà không sao.

Cách phát hiện đăng ký trùng trong dự án đang chạy:

csharp
var duplicates = builder.Services
    .GroupBy(d => (d.ServiceType, d.ImplementationType))
    .Where(g => g.Key.ImplementationType is not null && g.Count() > 1)
    .ToList();

foreach (var g in duplicates)
    Console.WriteLine($"TRÙNG {g.Count()} lần: {g.Key.ServiceType.Name} -> {g.Key.ImplementationType!.Name}");

Với AddOrderFeature() gọi hai lần, đoạn này in:

text
TRÙNG 2 lần: IValidator`1 -> OrderValidator

Đặt đoạn này ngay trước builder.Build() ở môi trường Development. Add không bao giờ báo lỗi khi trùng, ValidateOnBuild cũng không, nên đây là cách duy nhất trong bài để thấy đăng ký thừa.

Bài 3 — Decorator hai tầng ​

Bọc một repository bằng caching rồi bọc tiếp bằng logging, sao cho thứ tự là logging → caching → thật. Viết bằng container .NET thuần, rồi viết lại bằng Scrutor và so sánh.

Tiêu chí hoàn thành: thứ tự thực thi đúng như mô tả, và bạn nêu được vì sao container .NET thuần không có Decorate sẵn.

Gợi ý và lời giải — Bài 3

Gợi ý. Decorator là một lớp cài cùng interface và nhận chính interface đó qua constructor. Vấn đề với container .NET: khi LoggingRepo yêu cầu IOrderRepository, làm sao container biết phải đưa CachingRepo chứ không phải chính LoggingRepo?

Lời giải — các lớp:

csharp
public interface IOrderRepository { Task<Order?> GetAsync(int id, CancellationToken ct); }

public class EfOrderRepository(CrmDbContext db) : IOrderRepository
{
    public async Task<Order?> GetAsync(int id, CancellationToken ct)
        => await db.Orders.FindAsync([id], ct);
}

public class CachingOrderRepository(IOrderRepository inner, IMemoryCache cache) : IOrderRepository
{
    public async Task<Order?> GetAsync(int id, CancellationToken ct)
        => await cache.GetOrCreateAsync($"order:{id}", async e =>
        {
            e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            return await inner.GetAsync(id, ct);
        });
}

public class LoggingOrderRepository(IOrderRepository inner, ILogger<LoggingOrderRepository> log)
    : IOrderRepository
{
    public async Task<Order?> GetAsync(int id, CancellationToken ct)
    {
        log.LogInformation("Lấy đơn hàng {Id}", id);
        var sw = Stopwatch.StartNew();
        var order = await inner.GetAsync(id, ct);
        log.LogInformation("Xong sau {Ms} ms", sw.ElapsedMilliseconds);
        return order;
    }
}

Bằng container .NET thuần — phải tự dựng chuỗi bằng factory:

csharp
builder.Services.AddScoped<EfOrderRepository>();     // đăng ký theo KIỂU CỤ THỂ

builder.Services.AddScoped<IOrderRepository>(sp =>
{
    var that   = sp.GetRequiredService<EfOrderRepository>();
    var caching = new CachingOrderRepository(that, sp.GetRequiredService<IMemoryCache>());
    var logging = new LoggingOrderRepository(caching, sp.GetRequiredService<ILogger<LoggingOrderRepository>>());
    return logging;      // ngoài cùng là logging
});

Thứ tự thực thi: logging → caching → EfOrderRepository. Đúng yêu cầu.

Bằng Scrutor:

csharp
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.Decorate<IOrderRepository, CachingOrderRepository>();
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();

Lớp bọc sau nằm ở ngoài — nên thứ tự vẫn là logging → caching → thật.

So sánh:

Container thuầnScrutor
Số dòng~83
Tự dựng phụ thuộc của decoratorPhải làm tayTự động
Thêm một tầng bọcSửa lambdaThêm một dòng
Phụ thuộc thêmKhôngMột gói NuGet

Khác biệt lớn nhất nằm ở dòng thứ hai: với container thuần, mọi phụ thuộc của decorator phải tự lấy ra bằng GetRequiredService, và danh sách đó dài ra theo thời gian.

Vì sao container .NET không có Decorate sẵn. Đây là lựa chọn thiết kế có chủ ý: container tích hợp được giữ tối giản, chỉ cung cấp những gì framework cần. Các tính năng nâng cao — decorator, đăng ký theo quy ước, chặn lời gọi, đặt tên đăng ký — được để cho container bên thứ ba.

Đánh đổi này hợp lý: container mặc định nhẹ, khởi động nhanh, và ai cần thêm thì cài thêm.

Vì sao decorator đáng dùng. Nó là cách áp dụng nguyên tắc Open/Closed một cách rất sạch: thêm caching hay logging không sửa một dòng nào trong EfOrderRepository. Mỗi mối quan tâm nằm trong một lớp riêng, kiểm thử riêng được, và bật tắt bằng một dòng đăng ký.

Một lưu ý về thứ tự. Đặt caching bên trong logging nghĩa là log ghi lại cả những lần trúng cache — đó thường là điều bạn muốn, vì bạn cần biết tỉ lệ trúng cache. Đảo lại thì log chỉ ghi khi phải xuống database, và bạn mất thông tin đó. Thứ tự bọc là một quyết định có ý nghĩa, không tuỳ tiện.

Tự kiểm tra ​

Factory registration có bẫy gì?

Factory của một Singleton nhận root provider, nên resolve một service Scoped bên trong nó là captive dependency. ValidateOnBuild không nhìn vào bên trong lambda nên không bắt được lúc khởi động. ValidateScopes vẫn ném lỗi "Cannot resolve scoped service from root provider" ở lần resolve đầu tiên; khi tắt kiểm tra thì service Scoped bị giam vĩnh viễn và không bao giờ được Dispose. Quy tắc là trong factory của Singleton chỉ resolve Singleton.

Đăng ký instance có sẵn khác gì đăng ký kiểu?

Khi bạn đưa vào một instance đã tạo, container không coi mình là chủ sở hữu nên không gọi Dispose cho nó. Với đối tượng giữ tài nguyên thì nên dùng dạng factory để container quản lý vòng đời.

Open generic đăng ký thế nào và có giới hạn gì?

Dùng Add với typeof của interface mở và typeof của implementation mở, một dòng phục vụ mọi kiểu cụ thể. Đăng ký cụ thể sẽ thắng open generic cho đúng kiểu đó. Giới hạn là không có cú pháp generic nên phải viết typeof, và không dùng được với ràng buộc mà container không kiểm tra được.

Đăng ký nhiều implementation cho cùng một interface để làm gì?

Để tiêm cả tập vào IEnumerable của interface đó, ví dụ một nhóm luật kiểm tra hay nhóm handler. Thêm một luật mới chỉ cần thêm một dòng đăng ký mà không sửa lớp sử dụng, đúng tinh thần Open/Closed. Lưu ý GetRequiredService chỉ trả về đăng ký cuối cùng còn IEnumerable trả về tất cả.

TryAdd và TryAddEnumerable khác nhau ra sao?

TryAdd chỉ thêm nếu chưa có đăng ký nào cho ServiceType đó, dùng trong thư viện để ứng dụng đè lên được. TryAddEnumerable so cả ServiceType lẫn ImplementationType nên tránh nhân đôi khi một extension AddXxx bị gọi hai lần, tránh việc validator chạy hai lần.

Làm decorator bằng container .NET thuần thế nào?

Đăng ký lớp thật bằng kiểu cụ thể, rồi đăng ký interface bằng một factory tạo lớp bọc và truyền lớp thật vào làm inner. Cách này đọc được nhưng dài dòng khi có nhiều lớp cần bọc, khi đó Scrutor cho phép viết gọn bằng phương thức Decorate.

Kết luận ​

Ba điều đáng nhớ nhất:

  1. Factory của Singleton chỉ được resolve Singleton — ValidateOnBuild không kiểm tra được bên trong lambda, ValidateScopes chỉ báo lỗi lúc resolve.
  2. Nhiều implementation là tính năng: tiêm qua IEnumerable<T> thay cho switch-case.
  3. Thư viện dùng TryAdd, ứng dụng dùng Add, test dùng Replace.

Tham khảo ​

Điều hướng ​

Bài liên quan ​

Phát hành theo giấy phép MIT.