C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
240: Named and Keyed Services
I've seen this happen a dozen times: a developer needs to support multiple ways of doing the same thing. Maybe it's three different payment gateways, or two different storage providers. They register both implementations in the DI container, and then they're baffled when the application only ever uses one of them.
Take a look at this snippet. We have a notification system that should support both Email and SMS, depending on the user's preference.
public interface INotificationService {
void Send(string message);
}
public class EmailService : INotificationService {
public void Send(string message) => Console.WriteLine($"Email: {message}");
}
public class SmsService : INotificationService {
public void Send(string message) => Console.WriteLine($"SMS: {message}");
}
// In Program.cs
builder.Services.AddTransient<INotificationService, EmailService>();
builder.Services.AddTransient<INotificationService, SmsService>();
// In the Controller
public class NotificationController(INotificationService notificationService) : ControllerBase {
[HttpPost]
public IActionResult Notify(string msg) {
notificationService.Send(msg);
return Ok();
}
}
The "Last One Wins" Trap
If you run this, you'll notice that no matter what, it always sends an SMS. Why? Because when you register the same interface multiple times in the built-in .NET DI container, the container doesn't keep a list of "options" for a single injection point. Instead, the last registration simply overwrites the previous ones for any request for a single instance of that type.
You might think, "I'll just inject IEnumerable<INotificationService>," but that's a clunky workaround. It forces your controller to iterate through every service and manually check which one it needs using if statements or is checks. It leaks implementation details into your business logic, which is exactly what DI is supposed to prevent.
Solving it with Keyed Services
Starting in .NET 8, we have a first-class way to handle this: Keyed Services. Instead of just registering a type, we register a type associated with a specific key (usually a string or an enum). This allows us to tell the container, "I don't just want an INotificationService; I want the one labeled 'sms'."
Here is how we fix the registration:
// In Program.cs
builder.Services.AddKeyedTransient<INotificationService, EmailService>("email");
builder.Services.AddKeyedTransient<INotificationService, SmsService>("sms");
Now, the container knows the difference. But how do we get that specific service into our controller? We use the [FromKeyedServices] attribute in the constructor.
public class NotificationController : ControllerBase {
private readonly INotificationService _emailService;
private readonly INotificationService _smsService;
public NotificationController(
[FromKeyedServices("email")] INotificationService emailService,
[FromKeyedServices("sms")] INotificationService smsService)
{
_emailService = emailService;
_smsService = smsService;
}
[HttpPost]
public IActionResult Notify(string msg, string preference) {
if (preference == "email") _emailService.Send(msg);
else _smsService.Send(msg);
return Ok();
}
}
Telling the Container Exactly Which Implementation You Want
The magic here is that [FromKeyedServices] acts as a metadata hint to the IServiceProvider. When the controller is being instantiated, the DI container looks at the attribute, finds the registration that matches both the type INotificationService and the key "email", and injects that specific instance.
I should mention that keys don't have to be strings. You can use Enums, which I highly recommend for larger projects to avoid "magic string" typos that only crash your app at runtime. If you use an Enum, the registration looks like AddKeyedTransient<IService, Service>(ServiceType.Azure).
One final tip: if you still need a "default" service (one that works when no key is specified), you can still register a non-keyed version of the service alongside your keyed ones. The container is smart enough to keep them separate.
📋 Practical Task
Build a Multi-Provider Cloud Storage Switcher
You are building a file upload system that needs to support two different storage providers: Local Disk and Azure Blob Storage. The system must decide which one to use based on a configuration setting.
Requirements:
- Create an
IStorageProviderinterface with a methodvoid SaveFile(string fileName, byte[] data). - Implement
LocalStorageProviderandAzureStorageProvider. - In your
Program.cs, register both providers as Keyed Services using the keys"local"and"azure". - Create a
FileUploaderclass that accepts both services in its constructor using the[FromKeyedServices]attribute. - Implement a method in
FileUploadercalledUpload(string fileName, byte[] data, bool useCloud)that routes the file to the Azure provider ifuseCloudis true, and the Local provider otherwise.
Success Criteria: Your code should compile and successfully call the correct SaveFile implementation based on the useCloud boolean without using IEnumerable<IStorageProvider>.
There are no comments for now.