hoặc đổi sang Autofac. .NET 8 thêm keyed services: AddKeyedScoped("sendgrid") rồi lấy ra bằng [FromKeyedServices("sendgrid")]. Điều đáng nói là giới hạn của nó: khoá phải là hằng số lúc biên dịch trong attribute, nên chọn implementation theo dữ liệu lúc chạy vẫn cần IKeyedServiceProvider. Và IEnumerable thường không thấy service có khoá — một bất ngờ khó chịu nếu bạn đang dùng mẫu tiêm cả tập.">
Skip to content

7.8 - Keyed Services (.NET 8) ​

Tóm tắt: Trước .NET 8, container .NET không có cách chính thức để nói "cho tôi IEmailService, bản SendGrid". Mọi người phải tự chế Func<string, IEmailService> hoặc đổi sang Autofac. .NET 8 thêm keyed services: AddKeyedScoped<IEmailService, SendGridEmailService>("sendgrid") rồi lấy ra bằng [FromKeyedServices("sendgrid")]. Điều đáng nói là giới hạn của nó: khoá phải là hằng số lúc biên dịch trong attribute, nên chọn implementation theo dữ liệu lúc chạy vẫn cần IKeyedServiceProvider. Và IEnumerable<T> thường không thấy service có khoá — một bất ngờ khó chịu nếu bạn đang dùng mẫu tiêm cả tập.

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

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

  • Đăng ký và lấy ra service theo khoá.
  • Chọn implementation theo dữ liệu lúc chạy mà không dùng service locator.
  • Giải thích vì sao keyed service không xuất hiện trong IEnumerable<T>.
  • Biết khi nào keyed service là sai công cụ.

Cần biết trước: bài 7.2, bài 7.5 (nhiều implementation, IEnumerable<T>), enum và switch expression (bài 4.8). Attribute như [FromKeyedServices("smtp")] là nhãn gắn lên tham số để container đọc; đối số của attribute phải là hằng số lúc biên dịch.

Nội dung bài học ​

7.8.1 — Cách cũ trước .NET 8 ​

csharp
services.AddScoped<SmtpEmailService>();
services.AddScoped<SendGridEmailService>();

services.AddScoped<Func<string, IEmailService>>(sp => key => key switch
{
    "smtp"     => sp.GetRequiredService<SmtpEmailService>(),
    "sendgrid" => sp.GetRequiredService<SendGridEmailService>(),
    _          => throw new ArgumentException($"Không biết provider: {key}")
});

Chạy được, nhưng: khoá không kiểm tra được lúc biên dịch, lỗi chỉ hiện lúc chạy, và mỗi lần thêm provider phải sửa cả switch.

7.8.2 — Keyed services ​

csharp
services.AddKeyedScoped<IEmailService, SmtpEmailService>("smtp");
services.AddKeyedScoped<IEmailService, SendGridEmailService>("sendgrid");

services.AddKeyedSingleton<ICache, RedisCache>("distributed");
services.AddKeyedSingleton<ICache, MemoryCache>("local");

Lấy ra bằng attribute:

csharp
public class NotificationService(
    [FromKeyedServices("sendgrid")] IEmailService primary,
    [FromKeyedServices("smtp")]     IEmailService fallback,
    ILogger<NotificationService> logger)
{
    public async Task SendAsync(string to, string subject, string body, CancellationToken ct)
    {
        try
        {
            await primary.SendAsync(to, subject, body, ct);
        }
        catch (EmailServiceException ex)
        {
            logger.LogWarning(ex, "SendGrid lỗi, chuyển sang SMTP");
            await fallback.SendAsync(to, subject, body, ct);
        }
    }
}

Attribute này dùng được trong constructor, tham số của handler minimal API và tham số của action trong controller.

Khoá không nhất thiết là string — bất kỳ object nào có Equals và GetHashCode đúng, nên enum là lựa chọn tốt hơn:

csharp
public enum EmailProvider { Smtp, SendGrid }

services.AddKeyedScoped<IEmailService, SendGridEmailService>(EmailProvider.SendGrid);

enum cho bạn kiểm tra lúc biên dịch và đổi tên an toàn — thứ mà "sendgrid" không có.

7.8.3 — Chọn theo dữ liệu lúc chạy ​

Attribute cần hằng số lúc biên dịch, nên khi khoá đến từ dữ liệu (cấu hình của tenant, tham số request), dùng IKeyedServiceProvider:

csharp
public class TenantEmailService(IServiceProvider serviceProvider, ITenantContext tenant)
{
    public Task SendAsync(string to, string subject, string body, CancellationToken ct)
    {
        var service = serviceProvider.GetRequiredKeyedService<IEmailService>(tenant.EmailProvider);
        return service.SendAsync(to, subject, body, ct);
    }
}

Đây về hình thức là service locator, thứ bài 7.9 khuyên tránh. Khác biệt nằm ở phạm vi: lớp này chỉ resolve một kiểu duy nhất đã biết, và việc chọn khoá là nghiệp vụ thật sự của nó. Đó không phải một lớp giấu mọi phụ thuộc trong _sp.

Vẫn nên gói lại sau một interface hẹp để phần còn lại của hệ thống không thấy IServiceProvider:

csharp
public interface IEmailServiceResolver { IEmailService Resolve(EmailProvider provider); }

7.8.4 — Ba điều cần biết ​

1. IEnumerable<T> không thấy service có khoá.

csharp
services.AddScoped<IValidator, EmailValidator>();
services.AddKeyedScoped<IValidator, PhoneValidator>("phone");

public class Foo(IEnumerable<IValidator> validators) { }   // chỉ có EmailValidator

Service có khoá và không khoá nằm ở hai không gian tách biệt. GetKeyedServices<T>(key) trả về mọi service đăng ký cùng một khoá; muốn lấy mọi service có khoá bất kể khoá nào thì truyền KeyedService.AnyKey (Bài 2).

2. KeyedService.AnyKey để bắt mọi khoá.

csharp
services.AddKeyedScoped<IEmailService, DefaultEmailService>(KeyedService.AnyKey);

Bất kỳ khoá nào chưa có đăng ký riêng đều rơi vào đây — hữu ích cho fallback, nhưng cũng che mất lỗi gõ sai khoá. Từ .NET 10, GetKeyedService<T>(KeyedService.AnyKey) (xin một service bằng chính AnyKey) ném InvalidOperationException: KeyedService.AnyKey cannot be used to resolve a single service.

3. Biết khoá mình được resolve bằng.

csharp
public class EmailService([ServiceKey] string key) { }

7.8.5 — Khi nào không dùng keyed services ​

Keyed services giải quyết "nhiều implementation, chọn một". Nhưng có ba tình huống công cụ khác tốt hơn:

Tình huốngDùng gì
Cần chạy hết các implementation (validator, handler)IEnumerable<T>, không khoá — bài 7.5
Implementation chọn theo môi trường, cố định lúc khởi độngif (env.IsDevelopment()) trong Program.cs — đơn giản hơn nhiều
Mỗi implementation tự biết nó xử lý được gìStrategy pattern: IEnumerable<IPaymentStrategy> với CanHandle(type)

Trường hợp cuối đáng nói thêm:

csharp
public interface IPaymentStrategy
{
    bool CanHandle(PaymentMethod method);
    Task<PaymentResult> PayAsync(PaymentRequest request, CancellationToken ct);
}

public class PaymentService(IEnumerable<IPaymentStrategy> strategies)
{
    public Task<PaymentResult> PayAsync(PaymentRequest request, CancellationToken ct)
        => strategies.FirstOrDefault(s => s.CanHandle(request.Method))
               ?.PayAsync(request, ct)
           ?? throw new NotSupportedException($"Không hỗ trợ {request.Method}");
}

Ở đây implementation tự khai báo nó xử lý được gì, thay vì nơi gọi phải biết khoá. Thêm phương thức thanh toán mới = thêm một lớp và một dòng đăng ký, không đụng vào PaymentService. Với logic chọn phức tạp hơn một khoá, mẫu này rõ ràng hơn keyed services.

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

Danh sách rà soát keyed services

  • Khoá dùng enum hoặc const, không phải chuỗi rải rác trong code.
  • Không dùng keyed services cho trường hợp cần chạy hết mọi implementation.
  • Biết rằng IEnumerable<T> không nhìn thấy service có khoá.
  • Chọn theo dữ liệu lúc chạy được gói sau một interface hẹp, không lộ IServiceProvider.
  • Lựa chọn theo môi trường dùng if trong Program.cs thay vì keyed services.
  • Logic chọn phức tạp hơn một khoá thì dùng strategy pattern với CanHandle.
  • KeyedService.AnyKey chỉ dùng có chủ đích, không để che lỗi gõ sai khoá.

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

Bài 1 — Chuyển factory sang keyed services ​

Lấy một factory dạng Func<string, IFoo> đang có trong dự án và viết lại bằng keyed services với enum làm khoá. So sánh số dòng và số chỗ phải sửa khi thêm một cài đặt.

Tiêu chí hoàn thành: bạn nêu được lợi ích và một hạn chế của keyed services so với factory tự viết.

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

Gợi ý. Trước .NET 8, container tích hợp không hỗ trợ nhiều cài đặt phân biệt theo khoá, nên mọi người tự viết factory. Từ .NET 8 thì không cần nữa.

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

csharp
public delegate INotificationSender NotificationSenderFactory(string channel);

builder.Services.AddScoped<EmailSender>();
builder.Services.AddScoped<SmsSender>();
builder.Services.AddScoped<ZaloSender>();

builder.Services.AddScoped<NotificationSenderFactory>(sp => channel => channel switch
{
    "email" => sp.GetRequiredService<EmailSender>(),
    "sms"   => sp.GetRequiredService<SmsSender>(),
    "zalo"  => sp.GetRequiredService<ZaloSender>(),
    _ => throw new ArgumentOutOfRangeException(nameof(channel))
});

// Dùng
public class NotifyService(NotificationSenderFactory factory)
{
    public Task SendAsync(string channel, string content, CancellationToken ct)
        => factory(channel).SendAsync(content, ct);
}

Sau:

csharp
public enum Channel { Email, Sms, Zalo }

builder.Services.AddKeyedScoped<INotificationSender, EmailSender>(Channel.Email);
builder.Services.AddKeyedScoped<INotificationSender, SmsSender>(Channel.Sms);
builder.Services.AddKeyedScoped<INotificationSender, ZaloSender>(Channel.Zalo);

// Dùng — chọn lúc chạy
public class NotifyService(IServiceProvider sp)
{
    public Task SendAsync(Channel channel, string content, CancellationToken ct)
        => sp.GetRequiredKeyedService<INotificationSender>(channel).SendAsync(content, ct);
}

// Hoặc chọn lúc biên dịch, khi biết trước cần kênh nào
public class WelcomeService([FromKeyedServices(Channel.Email)] INotificationSender sender) { }

So sánh:

Factory tự viếtKeyed services
Số dòng đăng ký93
Thêm một kênh mớiSửa 2 chỗ: đăng ký + nhánh switchThêm 1 dòng
Khoá là enumPhải tự ánh xạ từ chuỗiHỗ trợ sẵn
Quên thêm nhánhLỗi lúc chạyLỗi lúc chạy

Dùng enum làm khoá thay vì chuỗi là cải thiện đáng kể: nó loại bỏ lỗi gõ sai, và cho phép switch vét cạn ở những chỗ khác trong hệ thống như bài 4.8 trình bày.

Hạn chế của keyed services. Cách dùng IServiceProvider ở trên đưa Service Locator quay lại — đúng thứ mà bài 7.9 cảnh báo. Chữ ký NotifyService(IServiceProvider sp) không cho biết lớp này cần gì.

Đây là một đánh đổi có thật, và cách giảm nhẹ là bọc việc tra cứu lại:

csharp
public interface INotificationSenderResolver
{
    INotificationSender Get(Channel channel);
}

internal sealed class NotificationSenderResolver(IServiceProvider sp) : INotificationSenderResolver
{
    public INotificationSender Get(Channel channel)
        => sp.GetRequiredKeyedService<INotificationSender>(channel);
}

// Giờ chữ ký nói đúng sự thật
public class NotifyService(INotificationSenderResolver resolver) { }

Việc tra cứu bị giới hạn trong một lớp hạ tầng nhỏ, còn tầng nghiệp vụ khai báo phụ thuộc tường minh. Đây là mẫu nên dùng khi cần chọn cài đặt theo dữ liệu lúc chạy.

Khi nào [FromKeyedServices] là đủ. Khi bạn biết lúc biên dịch cần cài đặt nào — ví dụ WelcomeService luôn gửi email. Lúc đó không cần tra cứu gì, và chữ ký vẫn hoàn toàn tường minh.

Bài 2 — Keyed services vô hình với IEnumerable ​

Đăng ký một IValidator không khoá và một IValidator có khoá, tiêm IEnumerable<IValidator> và đếm số phần tử.

Tiêu chí hoàn thành: bạn ghi lại đúng con số và nêu được hậu quả của nó trong một hệ thống thật.

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

Gợi ý. Keyed và không khoá là hai không gian đăng ký tách biệt. Hãy thử và xem con số.

Lời giải.

csharp
// IValidator.cs
public interface IValidator { string Name { get; } }
public class PlainValidator : IValidator { public string Name => "PlainValidator"; }
public class SpecialValidator : IValidator { public string Name => "SpecialValidator"; }
csharp
// Program.cs
using Microsoft.Extensions.DependencyInjection;

var s = new ServiceCollection();
s.AddSingleton<IValidator, PlainValidator>();                       // KHÔNG khoá
s.AddKeyedSingleton<IValidator, SpecialValidator>("special");       // CÓ khoá
var p = s.BuildServiceProvider();

Console.WriteLine($"IEnumerable<IValidator> thấy : {string.Join(", ", p.GetServices<IValidator>().Select(v => v.Name))}");
Console.WriteLine($"GetRequiredKeyedService      : {p.GetRequiredKeyedService<IValidator>("special").Name}");
Console.WriteLine($"GetKeyedServices(AnyKey)     : {string.Join(", ", p.GetKeyedServices<IValidator>(KeyedService.AnyKey).Select(v => v.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:

text
IEnumerable<IValidator> thấy : PlainValidator
GetRequiredKeyedService      : SpecialValidator
GetKeyedServices(AnyKey)     : SpecialValidator

Chỉ 1 phần tử. Đăng ký có khoá hoàn toàn vô hình với GetServices<T>() và với IEnumerable<T> tiêm qua constructor.

Hậu quả trong hệ thống thật. Mẫu "chạy tất cả validator" rất phổ biến:

csharp
public class OrderHandler(IEnumerable<IValidator<Order>> validators)
{
    public async Task<Result> HandleAsync(Order o, CancellationToken ct)
    {
        foreach (var v in validators)                    // chỉ chạy validator KHÔNG khoá
        {
            var result = await v.ValidateAsync(o, ct);
            if (!result.IsValid) return Result.Fail(result.Error);
        }
        // ...
    }
}

Nếu ai đó đăng ký một validator bằng AddKeyedScoped — có thể vì sao chép từ một chỗ khác — validator đó không bao giờ chạy. Không lỗi, không cảnh báo, chỉ đơn giản là quy tắc nghiệp vụ không được áp dụng.

Đây là loại lỗi đặc biệt nguy hiểm vì nó im lặng và vì nó ảnh hưởng tới tính đúng đắn nghiệp vụ, không phải tới kỹ thuật.

Lấy tất cả cài đặt có khoá. Dòng thứ ba ở trên cho thấy cách: GetKeyedServices<T>(KeyedService.AnyKey) trả về mọi đăng ký có khoá của T. Trên .NET 10, kết quả không gồm đăng ký dùng chính KeyedService.AnyKey làm khoá (đăng ký fallback ở mục 7.8.4); với gói .NET 9 thì có, nên đừng dựa vào điểm này nếu dự án còn chạy bản cũ.

Cách tường minh hơn, không phụ thuộc phiên bản, là liệt kê khoá rồi lấy từng cái:

csharp
var allSenders = Enum.GetValues<Channel>()
    .Select(k => sp.GetRequiredKeyedService<INotificationSender>(k))
    .ToList();

Hoặc đăng ký cả hai cách khi bạn cần cả hai kiểu truy cập:

csharp
builder.Services.AddScoped<EmailSender>();
builder.Services.AddKeyedScoped<INotificationSender>(Channel.Email, (sp, _) => sp.GetRequiredService<EmailSender>());
builder.Services.AddScoped<INotificationSender>(sp => sp.GetRequiredService<EmailSender>());

Cách này hơi dài dòng nhưng cho phép vừa GetServices vừa GetKeyedService, và vẫn chỉ có một thể hiện vì cả hai đều trỏ về EmailSender đã đăng ký.

Ba điều nữa cần biết về keyed services:

  1. Khoá so sánh bằng Equals. Dùng enum, string, hoặc record đều được. Dùng class thường thì gặp đúng vấn đề ở bài 1.5.
  2. KeyedService.AnyKey có hai vai: khi đăng ký, nó là fallback cho mọi khoá chưa có đăng ký riêng; khi gọi GetKeyedServices, nó nghĩa là "mọi khoá".
  3. Không phải container nào cũng hỗ trợ. Nếu dự án dùng Autofac hay một container khác, hãy kiểm tra trước khi chuyển sang keyed services.

Khi nào không nên dùng keyed services. Khi chỉ có một cài đặt — thêm khoá lúc đó chỉ làm code khó đọc mà không giải quyết gì. Và khi bạn cần chạy tất cả thay vì chọn một — lúc đó IEnumerable<T> với đăng ký không khoá mới là công cụ đúng.

Bài 3 — So sánh keyed services với mẫu chiến lược ​

Cài một hệ thống thanh toán hỗ trợ 3 phương thức, một lần bằng keyed services và một lần bằng mẫu chiến lược với CanHandle. Thêm phương thức thứ 4 và đếm số file phải sửa ở mỗi cách.

Tiêu chí hoàn thành: bạn nêu được tiêu chí chọn giữa hai cách, dựa trên cách chọn cài đặt chứ không dựa trên số dòng.

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

Gợi ý. Khác biệt cốt lõi: với keyed services, người gọi biết trước cần cài đặt nào. Với CanHandle, chính cài đặt tự quyết định nó có xử lý được hay không.

Lời giải — cách 1, keyed services:

csharp
public enum PaymentMethod { CreditCard, BankTransfer, EWallet }

builder.Services.AddKeyedScoped<IPaymentGateway, CreditCardGateway>(PaymentMethod.CreditCard);
builder.Services.AddKeyedScoped<IPaymentGateway, BankTransferGateway>(PaymentMethod.BankTransfer);
builder.Services.AddKeyedScoped<IPaymentGateway, EWalletGateway>(PaymentMethod.EWallet);

public class PaymentService(IPaymentGatewayResolver resolver)
{
    public Task<Result> PayAsync(Order o, PaymentMethod method, CancellationToken ct)
        => resolver.Get(method).PayAsync(o, ct);
}

Cách 2, chiến lược với CanHandle:

csharp
public interface IPaymentGateway
{
    bool CanHandle(Order o);
    Task<Result> PayAsync(Order o, CancellationToken ct);
}

public class CreditCardGateway : IPaymentGateway
{
    public bool CanHandle(Order o) => o.Method == PaymentMethod.CreditCard && o.Total <= 50_000_000m;
    // ...
}

builder.Services.AddScoped<IPaymentGateway, CreditCardGateway>();
builder.Services.AddScoped<IPaymentGateway, BankTransferGateway>();
builder.Services.AddScoped<IPaymentGateway, EWalletGateway>();

public class PaymentService(IEnumerable<IPaymentGateway> gateways)
{
    public Task<Result> PayAsync(Order o, CancellationToken ct)
    {
        var gateway = gateways.FirstOrDefault(g => g.CanHandle(o))
                 ?? throw new InvalidOperationException("Không có cổng thanh toán phù hợp");
        return gateway.PayAsync(o, ct);
    }
}

Thêm phương thức thứ 4:

Keyed servicesChiến lược CanHandle
Thêm lớp cài đặt1 file mới1 file mới
Thêm giá trị enum1 file sửaKhông cần
Thêm dòng đăng ký1 dòng1 dòng
Tổng file sửa21

Nhưng số file không phải tiêu chí chọn. Tiêu chí thật là ai quyết định dùng cài đặt nào:

Keyed servicesChiến lược CanHandle
Ai chọnNgười gọi, qua khoáChính cài đặt, qua điều kiện
Điều kiện chọnMột giá trị đơn giảnLogic tuỳ ý trên dữ liệu
Nhìn code biết cài đặt nào chạyCó — theo khoáKhông — phải đọc mọi CanHandle
Hai cài đặt cùng nhậnKhông xảy raCó thể — phụ thuộc thứ tự đăng ký
Không cài đặt nào nhậnLỗi rõ ràng từ containerPhải tự xử lý

Chọn keyed services khi tiêu chí chọn là một giá trị đơn giản mà người gọi đã biết: loại tài liệu, mã quốc gia, tên nhà cung cấp. Ưu điểm lớn nhất là nhìn vào khoá là biết ngay cài đặt nào sẽ chạy.

Chọn CanHandle khi điều kiện phụ thuộc vào nhiều thuộc tính của dữ liệu, như ví dụ trên: thẻ tín dụng chỉ xử lý được đơn dưới 50 triệu. Khoá đơn giản không diễn đạt được điều kiện này.

Cái bẫy của CanHandle — thứ tự quyết định kết quả. Với FirstOrDefault, cài đặt đăng ký trước sẽ thắng nếu cả hai cùng nhận. Điều đó biến thứ tự đăng ký thành một phần của logic nghiệp vụ, mà lại nằm ở Program.cs chứ không nằm cùng với nghiệp vụ.

Nếu buộc dùng CanHandle, hãy làm thứ tự thành tường minh:

csharp
public interface IPaymentGateway
{
    int Priority { get; }          // thấp hơn được xét trước
    bool CanHandle(Order o);
    Task<Result> PayAsync(Order o, CancellationToken ct);
}

var gateway = gateways.OrderBy(g => g.Priority).FirstOrDefault(g => g.CanHandle(o));

Lúc này thứ tự nằm trong chính cài đặt, không phụ thuộc vào cách đăng ký.

Tự kiểm tra ​

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

Trước .NET 8, container .NET không có cách chính thức để nói cho tôi interface này nhưng là implementation cụ thể kia, nên mọi người phải tự chế Func factory hoặc đổi sang Autofac. Keyed services cho phép đăng ký nhiều implementation cùng interface theo khoá và lấy ra bằng FromKeyedServices.

Nên dùng string hay enum làm khoá?

Enum, vì khoá có thể là bất kỳ object nào có Equals và GetHashCode đúng. Enum cho bạn kiểm tra lúc biên dịch và đổi tên an toàn, còn chuỗi thì gõ sai chỉ phát hiện lúc chạy.

Chọn implementation theo dữ liệu lúc chạy thì làm sao?

Attribute cần hằng số lúc biên dịch nên phải dùng GetRequiredKeyedService từ IServiceProvider. Về hình thức đó là service locator, nhưng khác ở chỗ lớp này chỉ resolve một kiểu duy nhất đã biết và việc chọn khoá là nghiệp vụ thật của nó. Nên gói lại sau một interface hẹp để phần còn lại của hệ thống không thấy IServiceProvider.

Vì sao IEnumerable<T> không thấy service có khoá?

Vì service có khoá và không khoá nằm ở hai không gian tách biệt trong container. GetKeyedServices với một khoá cụ thể trả về các service cùng khoá đó; truyền KeyedService.AnyKey thì nhận mọi service có khoá (trên .NET 10 không gồm đăng ký fallback AnyKey).

Khi nào không nên dùng keyed services?

Ba trường hợp. Khi cần chạy hết mọi implementation như validator hay handler thì dùng IEnumerable không khoá. Khi implementation chọn theo môi trường và cố định lúc khởi động thì một câu if trong Program.cs đơn giản hơn. Và khi logic chọn phức tạp hơn một khoá thì dùng strategy pattern.

Strategy pattern hơn keyed services ở điểm nào?

Implementation tự khai báo nó xử lý được gì qua phương thức CanHandle, thay vì nơi gọi phải biết khoá. Thêm một trường hợp mới chỉ cần thêm một lớp và một dòng đăng ký, không đụng vào lớp điều phối. Với logic chọn phức tạp thì mẫu này rõ ràng hơn.

Kết luận ​

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

  1. Dùng enum làm khoá, không dùng chuỗi rải rác.
  2. IEnumerable<T> không thấy service có khoá — hai không gian tách biệt.
  3. Cần chạy hết thì dùng IEnumerable; chọn phức tạp thì dùng strategy. Keyed services chỉ cho "chọn một theo khoá".

Tham khảo ​

Điều hướng ​

Bài liên quan ​

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