Skip to main content

7.11 — Mở rộng và đào sâu

Summary

Những kỹ thuật DI nối tiếp Module 7, kèm mức độ ưu tiên. Kỹ thuật có tỷ lệ lợi ích trên công sức cao nhất là decorator: thêm caching, logging hoặc retry vào một service mà không sửa một dòng nào của service đó — và nó đúng nguyên tắc mở/đóng theo cách cụ thể nhất. Kỹ thuật bị đánh giá thấp nhất là module đăng ký: gom AddScoped theo tính năng thay vì để 200 dòng trong Program.cs, rẻ để làm và cải thiện rõ khả năng đọc. Và một lời khuyên đi ngược xu hướng: container bên thứ ba như Autofac gần như không còn cần thiết — container tích hợp của .NET đã đủ cho hầu hết nhu cầu, và Scrutor bù nốt phần còn thiếu.

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

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

  • Thêm hành vi vào service bằng decorator qua DI.
  • Dùng factory khi cần chọn cài đặt lúc chạy.
  • Tổ chức đăng ký service theo module.
  • Quyết định có cần container bên thứ ba không.

Nội dung bài học​

7.11.1 — Decorator: ưu tiên cao​

Điều kiện kích hoạt: cần thêm caching, logging, retry hoặc đo lường vào một service có sẵn.

// Service gốc — KHÔNG sửa gì
public sealed class LeadRepository(AppDbContext db) : ILeadRepository
{
public async Task<Lead?> GetByIdAsync(LeadId id, CancellationToken ct)
=> await db.Leads.FindAsync([id], ct);
}

// Decorator — bao bên ngoài, CÙNG interface
public sealed class CachedLeadRepository(
ILeadRepository inner,
IMemoryCache cache) : ILeadRepository
{
public async Task<Lead?> GetByIdAsync(LeadId id, CancellationToken ct)
{
if (cache.TryGetValue(id, out Lead? cached))
return cached;

var lead = await inner.GetByIdAsync(id, ct);

if (lead is not null)
cache.Set(id, lead, TimeSpan.FromMinutes(5));

return lead;
}
}

Đăng ký bằng Scrutor:

builder.Services.AddScoped<ILeadRepository, LeadRepository>();
builder.Services.Decorate<ILeadRepository, CachedLeadRepository>();
builder.Services.Decorate<ILeadRepository, LoggedLeadRepository>();

// Chuỗi thực tế: Logged -> Cached -> Repository

Mọi nơi tiêm ILeadRepository tự động nhận chuỗi decorator mà không biết và không cần biết.

Ba lợi ích cụ thể:

  • LeadRepository không hề đổi — nó vẫn là lớp đơn giản chỉ làm một việc.
  • Bỏ caching là xoá một dòng đăng ký.
  • Test LeadRepository không phải quan tâm tới cache, và test CachedLeadRepository dùng một fake đơn giản cho inner.

Thứ tự đăng ký quan trọng: Decorate gọi sau sẽ bọc ngoài cùng. Trong ví dụ trên, log ghi lại cả những lần cache hit — thường là điều bạn muốn.

Không có Scrutor thì đăng ký thủ công được, chỉ dài hơn:

builder.Services.AddScoped<LeadRepository>();
builder.Services.AddScoped<ILeadRepository>(sp =>
new CachedLeadRepository(
sp.GetRequiredService<LeadRepository>(),
sp.GetRequiredService<IMemoryCache>()));

7.11.2 — Factory: khi chọn lúc chạy​

Điều kiện kích hoạt: cài đặt được chọn dựa trên dữ liệu chỉ biết lúc chạy.

// Keyed services (.NET 8+) — cách đơn giản nhất
builder.Services.AddKeyedScoped<IPaymentGateway, VnPayGateway>("vnpay");
builder.Services.AddKeyedScoped<IPaymentGateway, MomoGateway>("momo");

public class PaymentService([FromKeyedServices("vnpay")] IPaymentGateway gateway)

Khi khoá chỉ biết lúc chạy, dùng factory:

public interface IPaymentGatewayFactory
{
IPaymentGateway Create(string providerCode);
}

public sealed class PaymentGatewayFactory(IServiceProvider provider) : IPaymentGatewayFactory
{
public IPaymentGateway Create(string providerCode)
=> provider.GetRequiredKeyedService<IPaymentGateway>(providerCode);
}

// Su dung
var gateway = _factory.Create(donHang.NhaCungCapThanhToan);
await gateway.ChargeAsync(donHang, ct);

Cảnh báo: tiêm IServiceProvider trực tiếp vào service nghiệp vụ là anti-pattern service locator — nó giấu dependency thật và làm test khó (bài 7.10). Bọc nó trong một factory có interface rõ ràng như trên thì chấp nhận được, vì dependency vẫn tường minh ở chỗ dùng.

7.11.3 — Module đăng ký: ưu tiên cao​

Điều kiện kích hoạt: Program.cs vượt quá khoảng 50 dòng đăng ký.

// Program.cs — khó đọc khi có 200 dòng như này
builder.Services.AddScoped<ILeadRepository, LeadRepository>();
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
// ... 198 dòng nữa
// Gom theo tính năng
namespace Crm.Sales;

public static class SalesModule
{
public static IServiceCollection AddSales(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<ILeadRepository, LeadRepository>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
services.AddScoped<ILeadConversionService, LeadConversionService>();

services.AddOptions<SalesOptions>()
.Bind(config.GetSection("Sales"))
.ValidateOnStart();

return services;
}
}
// Program.cs — đọc được
builder.Services
.AddSales(builder.Configuration)
.AddBilling(builder.Configuration)
.AddNotifications(builder.Configuration);

Lợi ích ngoài khả năng đọc: mỗi module tự chứa mọi thứ nó cần, nên tách module thành service riêng sau này dễ hơn nhiều (bài 18.2).

7.11.4 — Đăng ký theo quy ước với Scrutor​

Điều kiện kích hoạt: có hàng chục lớp theo cùng một mẫu đặt tên.

builder.Services.Scan(scan => scan
.FromAssemblyOf<LeadRepository>()
.AddClasses(c => c.AssignableTo(typeof(IRepository<>)))
.AsImplementedInterfaces()
.WithScopedLifetime());

Thay 40 dòng AddScoped bằng một khối.

Đánh đổi thật: bạn không còn thấy service nào được đăng ký khi đọc Program.cs. Khi có lỗi "không tìm thấy service", việc chẩn đoán khó hơn vì không có dòng nào để đặt breakpoint.

Cách cân bằng: dùng quét cho nhóm lớp đồng nhất và nhiều (repository, handler, validator), đăng ký tường minh cho service có cấu hình đặc biệt hoặc vòng đời khác thường.

7.11.5 — Container bên thứ ba: thường không cần​

Autofac, Lamar và các container khác từng cần thiết vì container tích hợp của .NET thiếu nhiều tính năng. Điều đó không còn đúng:

Tính năngContainer .NETCần bên thứ ba?
Vòng đời cơ bảnCóKhông
Keyed servicesCó (.NET 8+)Không
Nhiều cài đặt cho một interfaceCó (IEnumerable<T>)Không
Generic không ràng buộcCóKhông
DecoratorScrutorKhông
Quét assemblyScrutorKhông
Property injectionKhôngCó — nhưng đó là anti-pattern
Interception độngKhôngCó — nhưng thường có cách khác

Hai hàng cuối là lý do duy nhất còn lại, và cả hai đều đáng cân nhắc lại:

  • Property injection giấu dependency, làm đối tượng có thể tồn tại ở trạng thái chưa đủ điều kiện hoạt động. Constructor injection tốt hơn về mọi mặt.
  • Interception động (chèn hành vi vào mọi lời gọi qua proxy) mạnh nhưng làm luồng thực thi khó theo dõi. Decorator tường minh thường rõ ràng hơn, và pipeline behavior của MediatR giải quyết phần lớn nhu cầu (bài 16.7).

Khuyến nghị: container tích hợp + Scrutor đủ cho hầu hết dự án .NET hiện nay. Thêm một container khác là thêm một thứ cả đội phải học.

7.11.6 — Thứ tự nên học​

Ưu tiênKỹ thuậtVì sao
CaoDecoratorThêm hành vi không sửa code gốc
CaoModule đăng kýRẻ, cải thiện rõ khả năng đọc
CaoKeyed servicesThay factory trong trường hợp đơn giản
Trung bìnhFactory có interfaceKhi chọn cài đặt lúc chạy
Trung bìnhQuét theo quy ướcKhi có nhiều lớp đồng nhất
ThấpContainer bên thứ baHiếm khi còn cần

Sau Module 7, bước tiếp theo là Module 8 — ASP.NET Core Fundamentals.

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

Danh sách rà soát DI nâng cao

  • •Caching và logging thêm bằng decorator, không sửa service gốc.
  • •Không tiêm IServiceProvider trực tiếp vào service nghiệp vụ.
  • •Factory có interface rõ ràng thay vì service locator.
  • •Đăng ký service gom theo module, Program.cs ngắn gọn.
  • •Mỗi module tự chứa cả đăng ký service lẫn cấu hình Options.
  • •Quét theo quy ước chỉ dùng cho nhóm lớp đồng nhất.
  • •Đã cân nhắc kỹ trước khi thêm container bên thứ ba.

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

Bài 1 — Thêm decorator​

Cài Scrutor và thêm một decorator ghi log thời gian cho một repository. Xác nhận service gốc không đổi.

Tiêu chí hoàn thành: repository gốc không có một dòng nào thay đổi, và bạn biết thứ tự đăng ký quyết định thứ tự các lớp bọc.

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

Gợi ý. Decorator nhận chính interface mà nó cài đặt. Đó là điểm khiến nó xếp chồng được.

Lời giải:

// Service gốc — KHÔNG đổi gì
public sealed class KhoLead(CrmDbContext db) : IKhoLead
{
public async Task<Lead?> TimAsync(int id, CancellationToken ct)
=> await db.Leads.FindAsync([id], ct);
}
// Decorator — nhận IKhoLead, và cũng LÀ IKhoLead
public sealed class KhoLeadCoLog(IKhoLead trong, ILogger<KhoLeadCoLog> log) : IKhoLead
{
public async Task<Lead?> TimAsync(int id, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
try
{
return await trong.TimAsync(id, ct);
}
finally
{
if (sw.ElapsedMilliseconds > 100)
log.LogWarning("TimAsync({Id}) mất {Ms} ms", id, sw.ElapsedMilliseconds);
}
}
}
builder.Services.AddScoped<IKhoLead, KhoLead>();
builder.Services.Decorate<IKhoLead, KhoLeadCoLog>(); // Scrutor
Chỗ dùng KHÔNG đổi:
public class LeadService(IKhoLead kho) { }

Nó vẫn nhận IKhoLead. Nó không biết, và không cần biết,
rằng giờ có một lớp log nằm giữa.

Và đây là điều decorator giải quyết mà kế thừa không giải quyết được:

Không sửa KhoLead    -> test của KhoLead vẫn xanh, không phải chạy lại
Không sửa LeadService -> không có rủi ro nào ở chỗ dùng
Gỡ ra bằng cách xoá MỘT dòng đăng ký

Xếp chồng nhiều decorator — thứ tự quan trọng:

builder.Services.AddScoped<IKhoLead, KhoLead>();
builder.Services.Decorate<IKhoLead, KhoLeadCoCache>(); // đăng ký TRƯỚC
builder.Services.Decorate<IKhoLead, KhoLeadCoLog>(); // đăng ký SAU
Lớp đăng ký SAU nằm NGOÀI CÙNG:

LeadService
-> KhoLeadCoLog (ngoài cùng, đăng ký cuối)
-> KhoLeadCoCache
-> KhoLead (gốc)

Thứ tự này có hệ quả thật:

Log ngoài cache: log đo TỔNG thời gian, kể cả khi lấy từ cache
-> thấy được "endpoint này nhanh nhờ cache"

Cache ngoài log: log chỉ chạy khi cache miss
-> không biết tỉ lệ cache hit là bao nhiêu
Nói chung: đặt thứ bạn muốn ĐO ở ngoài cùng.

Ba decorator hay dùng, và chúng minh hoạ rõ vì sao mẫu này đáng biết:

// 1. Cache
public sealed class KhoLeadCoCache(IKhoLead trong, IMemoryCache cache) : IKhoLead
{
public async Task<Lead?> TimAsync(int id, CancellationToken ct)
=> await cache.GetOrCreateAsync($"lead:{id}", async e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
e.Size = 1;
return await trong.TimAsync(id, ct);
});
}

// 2. Thử lại
public sealed class KhoLeadCoRetry(IKhoLead trong, ResiliencePipeline pipeline) : IKhoLead
{
public async Task<Lead?> TimAsync(int id, CancellationToken ct)
=> await pipeline.ExecuteAsync(async c => await trong.TimAsync(id, c), ct);
}

// 3. Kiểm tra quyền
public sealed class KhoLeadCoQuyen(IKhoLead trong, INguoiDungHienTai nd) : IKhoLead
{
public async Task<Lead?> TimAsync(int id, CancellationToken ct)
{
var lead = await trong.TimAsync(id, ct);
return lead?.TenantId == nd.TenantId ? lead : null;
}
}
Ba mối quan tâm khác nhau, ba lớp riêng, và KhoLead vẫn chỉ làm một việc:
đọc ghi database.

Khi nào KHÔNG dùng decorator:

Tình huốngVì sao
Interface có 15 phương thứcMỗi decorator phải cài lại cả 15, kể cả khi chỉ quan tâm 1
Mối quan tâm cần dữ liệu nội bộ của lớp gốcDecorator chỉ thấy tham số và kết quả
Chỉ có một chỗ cầnViết thẳng vào lớp gốc đơn giản hơn
Với interface lớn, cân nhắc:
- tách interface nhỏ hơn (nguyên tắc phân tách interface)
- hoặc dùng middleware pipeline (MediatR behavior) thay vì decorator

Và một cách kiểm chứng decorator đang hoạt động:

[Fact]
public void IKhoLead_duoc_boc_boi_log_va_cache()
{
using var scope = _factory.Services.CreateScope();
var kho = scope.ServiceProvider.GetRequiredService<IKhoLead>();

kho.Should().BeOfType<KhoLeadCoLog>("decorator đăng ký cuối phải nằm ngoài cùng");
}
Test này bắt được lỗi hay gặp: ai đó thêm một AddScoped<IKhoLead, KhoLead>()
ở dưới dòng Decorate -> đăng ký sau ghi đè -> mọi decorator biến mất âm thầm.

Bài 2 — Gom module​

Tách đăng ký service trong Program.cs thành hai module theo tính năng và đo số dòng còn lại.

Tiêu chí hoàn thành: Program.cs ngắn hơn rõ rệt, và mỗi module đặt cạnh code mà nó đăng ký.

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

Gợi ý. Chia theo tính năng, không theo loại. Gom mọi AddScoped vào một chỗ không giúp gì.

Lời giải — trước:

// Program.cs — 140 dòng, trong đó 90 dòng là đăng ký
builder.Services.AddScoped<IKhoLead, KhoLead>();
builder.Services.AddScoped<ILeadService, LeadService>();
builder.Services.AddScoped<IValidator<TaoLeadRequest>, TaoLeadValidator>();
builder.Services.AddScoped<IKhoKhachHang, KhoKhachHang>();
builder.Services.AddScoped<IKhachHangService, KhachHangService>();
builder.Services.AddScoped<IKhoHoaDon, KhoHoaDon>();
builder.Services.AddScoped<IHoaDonService, HoaDonService>();
builder.Services.AddScoped<IPdfService, PdfService>();
// ... 82 dòng nữa

Sau:

// src/Crm.Api/Features/Leads/LeadsModule.cs — ĐẶT CẠNH code của tính năng
public static class LeadsModule
{
public static IServiceCollection ThemTinhNangLead(this IServiceCollection services)
{
services.AddScoped<IKhoLead, KhoLead>();
services.AddScoped<ILeadService, LeadService>();
services.AddScoped<IValidator<TaoLeadRequest>, TaoLeadValidator>();
services.Decorate<IKhoLead, KhoLeadCoCache>();
return services;
}
}
// Program.cs — giờ đọc như một mục lục
builder.Services
.ThemHaTang(builder.Configuration)
.ThemTinhNangLead()
.ThemTinhNangKhachHang()
.ThemTinhNangHoaDon()
.ThemTinhNangBaoCao();

var app = builder.Build();
Program.cs: 140 dòng -> 48 dòng
Và 48 dòng còn lại nói được toàn bộ hình dạng của ứng dụng.

Bốn lợi ích, và cái thứ ba là cái ít được nói tới:

1. Program.cs đọc được — nó liệt kê các tính năng, không liệt kê các lớp.

2. Đăng ký nằm cạnh code mà nó đăng ký.

Thêm một service cho tính năng Lead -> sửa file trong thư mục Leads
-> không phải mở Program.cs -> không tạo xung đột merge với đội khác

3. Xung đột merge giảm hẳn.

Trước: ba người thêm service cùng lúc -> ba người sửa Program.cs
-> conflict ở cùng vùng dòng, mỗi lần merge

Sau: mỗi người sửa module của tính năng mình -> không chạm nhau

4. Xoá một tính năng là xoá một thư mục và một dòng.

Với Program.cs tập trung, xoá tính năng nghĩa là đi tìm 8–10 dòng
rải rác giữa 90 dòng — và thường sót vài dòng, để lại đăng ký chết.

Và một quy ước đáng theo: module KHÔNG tự đăng ký hạ tầng dùng chung.

// SAI — mỗi module tự thêm DbContext
public static IServiceCollection ThemTinhNangLead(this IServiceCollection s)
{
s.AddDbContext<CrmDbContext>(...); // ba module cùng làm -> ba lần đăng ký
...
}

// ĐÚNG — hạ tầng ở một module riêng, gọi một lần
public static IServiceCollection ThemHaTang(this IServiceCollection s, IConfiguration c)
{
s.AddDbContext<CrmDbContext>(o => o.UseSqlServer(c.GetConnectionString("Db")));
s.AddStackExchangeRedisCache(o => o.Configuration = c["Redis"]);
s.AddSingleton(TimeProvider.System);
return s;
}

Đăng ký theo quy ước với Scrutor — khi số lớp lớn:

services.Scan(scan => scan
.FromAssemblyOf<LeadService>()
.AddClasses(c => c.AssignableTo(typeof(IKho<>)))
.AsImplementedInterfaces()
.WithScopedLifetime());
Tiện khi có 30 repository theo cùng một khuôn.

Nhưng đánh đổi: không còn tìm được "ai đăng ký cái này" bằng grep
-> với đội mới vào, đăng ký tường minh dễ lần hơn nhiều

Quy tắc thực dụng: quét theo quy ước cho những nhóm ĐỀU ĐẶN và LỚN,
đăng ký tường minh cho phần còn lại.

Và một kiểm chứng đáng có sau khi tách module:

[Fact]
public void Moi_module_dang_ky_du_phu_thuoc_cua_no()
{
var services = new ServiceCollection();
services.ThemHaTang(_cauHinh);
services.ThemTinhNangLead();

var sp = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = true,
ValidateOnBuild = true,
});

using var scope = sp.CreateScope();
scope.ServiceProvider.GetRequiredService<ILeadService>().Should().NotBeNull();
}
Test này dựng CHỈ module Lead cộng hạ tầng — không có module khác.
-> bắt được trường hợp module Lead âm thầm phụ thuộc vào một service
do module Hoá đơn đăng ký

Bài 3 — So sánh factory​

Cài cùng một nhu cầu bằng keyed services và bằng factory, so sánh code ở chỗ dùng.

Tiêu chí hoàn thành: bạn có cả hai bản chạy được, và biết khi nào keyed services không dùng được.

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

Gợi ý. Keyed services chọn theo một khoá biết trước. Factory chọn theo bất kỳ logic nào.

Lời giải — keyed services (.NET 8 trở lên):

builder.Services.AddKeyedScoped<IGuiThongBao, GuiEmail>("email");
builder.Services.AddKeyedScoped<IGuiThongBao, GuiSms>("sms");
builder.Services.AddKeyedScoped<IGuiThongBao, GuiZalo>("zalo");
// Chỗ dùng — khoá biết lúc biên dịch
public class ThongBaoService([FromKeyedServices("email")] IGuiThongBao email)
{
public Task GuiAsync(string den, string noiDung, CancellationToken ct)
=> email.GuiAsync(den, noiDung, ct);
}
// Hoặc chọn lúc chạy
public class ThongBaoService(IServiceProvider sp)
{
public Task GuiAsync(string kenh, string den, string noiDung, CancellationToken ct)
=> sp.GetRequiredKeyedService<IGuiThongBao>(kenh).GuiAsync(den, noiDung, ct);
}

Với factory:

public interface IGuiThongBaoFactory { IGuiThongBao Tao(KenhThongBao kenh); }

public sealed class GuiThongBaoFactory(IServiceProvider sp) : IGuiThongBaoFactory
{
public IGuiThongBao Tao(KenhThongBao kenh) => kenh switch
{
KenhThongBao.Email => sp.GetRequiredService<GuiEmail>(),
KenhThongBao.Sms => sp.GetRequiredService<GuiSms>(),
KenhThongBao.Zalo => sp.GetRequiredService<GuiZalo>(),
_ => throw new ArgumentOutOfRangeException(nameof(kenh)),
};
}
public class ThongBaoService(IGuiThongBaoFactory factory)
{
public Task GuiAsync(KenhThongBao kenh, string den, string noiDung, CancellationToken ct)
=> factory.Tao(kenh).GuiAsync(den, noiDung, ct);
}

So sánh:

Keyed servicesFactory
Code phải viếtÍt hơn — không có lớp factoryMột interface + một lớp
Khoástring hoặc objectBất kỳ kiểu nào, kể cả enum
Chọn theo logic phức tạpKhôngCó
Kiểm tra lúc biên dịchKhông — khoá là chuỗiCó — enum vét cạn được
Test chỗ dùngPhải dựng container có keyedGiả lập factory, đơn giản hơn
Lỗi khi khoá saiLúc chạyLúc chạy, nhưng enum chặn phần lớn

Và đây là ba trường hợp keyed services KHÔNG dùng được:

1. Việc chọn cần logic, không chỉ tra khoá.

public IGuiThongBao Tao(KhachHang kh, LoaiThongBao loai)
{
if (loai == LoaiThongBao.Khẩn && kh.CoSoDienThoai) return _sms;
if (kh.DaDongYNhanEmail) return _email;
if (kh.CoZalo) return _zalo;
return _khongGui;
}
Điều kiện phụ thuộc vào DỮ LIỆU, không phải một khoá biết trước
-> keyed services không diễn đạt được

2. Cần tạo nhiều instance với tham số khác nhau.

public IKetNoiSftp Tao(string host, int cong, string nguoiDung)
=> new KetNoiSftp(host, cong, nguoiDung, _log);
DI container không tạo được instance với tham số động.
Factory là cách duy nhất.

3. Cần biết TẤT CẢ cài đặt, không chỉ một.

// Trường hợp này thì cả hai đều thua một cách đơn giản hơn:
public class ThongBaoService(IEnumerable<IGuiThongBao> tatCa)
{
public async Task GuiTatCaAsync(string den, string nd, CancellationToken ct)
{
foreach (var g in tatCa) await g.GuiAsync(den, nd, ct);
}
}
Tiêm IEnumerable<T> lấy MỌI cài đặt đã đăng ký — không cần factory, không cần khoá.
Đây là cách dùng cho mẫu chiến lược ở bài 5.10.

Quy tắc chọn:

Chọn theo một khoá cố định, biết lúc biên dịch  -> keyed services
Chọn theo enum, muốn trình biên dịch kiểm tra -> factory với switch vét cạn
Chọn theo dữ liệu hoặc logic -> factory
Cần tạo với tham số động -> factory
Cần tất cả cài đặt -> IEnumerable<T>

Và một lưu ý về keyed services: khoá chuỗi không có kiểm tra.

sp.GetRequiredKeyedService<IGuiThongBao>("emial");      // gõ sai -> lỗi lúc chạy
// Giảm rủi ro bằng hằng số
public static class KenhKeys
{
public const string Email = "email";
public const string Sms = "sms";
}

sp.GetRequiredKeyedService<IGuiThongBao>(KenhKeys.Email);
Hoặc dùng enum làm khoá — keyed services chấp nhận object:
builder.Services.AddKeyedScoped<IGuiThongBao, GuiEmail>(KenhThongBao.Email);
sp.GetRequiredKeyedService<IGuiThongBao>(KenhThongBao.Email);

-> lấy lại được kiểm tra lúc biên dịch, mà vẫn không cần viết factory.

Tự kiểm tra​

Frequently asked questions

Decorator giải quyết vấn đề gì?

Nó cho phép thêm caching, logging hay retry vào một service mà không sửa dòng nào của service đó. Service gốc vẫn đơn giản, bỏ hành vi thêm vào chỉ là xoá một dòng đăng ký, và test của cả hai đều đơn giản.

Thứ tự đăng ký decorator ảnh hưởng thế nào?

Lệnh Decorate gọi sau sẽ bọc ngoài cùng. Nếu đăng ký cache trước rồi log sau, log sẽ ghi lại cả những lần cache hit, thường là điều bạn muốn.

Vì sao tiêm IServiceProvider vào service nghiệp vụ là anti-pattern?

Vì nó giấu dependency thật, khiến đọc constructor không biết lớp cần gì, và làm test khó vì phải dựng cả container. Bọc nó trong một factory có interface rõ ràng thì chấp nhận được.

Lợi ích ngoài khả năng đọc của module đăng ký là gì?

Mỗi module tự chứa mọi thứ nó cần gồm cả đăng ký service lẫn cấu hình Options, nên việc tách module thành service riêng sau này dễ hơn nhiều.

Đánh đổi của việc quét assembly theo quy ước là gì?

Bạn không còn thấy service nào được đăng ký khi đọc Program.cs, nên khi có lỗi không tìm thấy service thì khó chẩn đoán hơn vì không có dòng nào để đặt breakpoint.

Còn lý do nào để dùng container bên thứ ba không?

Chỉ còn property injection và interception động. Nhưng property injection giấu dependency và làm đối tượng có thể tồn tại ở trạng thái chưa đủ điều kiện, còn interception động làm luồng thực thi khó theo dõi hơn decorator tường minh.

Kết luận​

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

  1. Decorator thêm hành vi mà không sửa code gốc — kỹ thuật đáng dùng nhất trong bài.
  2. Module đăng ký rẻ và cải thiện rõ, đồng thời chuẩn bị cho việc tách service.
  3. Container tích hợp cộng Scrutor đủ cho hầu hết dự án .NET hiện nay.

Tham khảo​

Điều hướng​