How Keyed Service Dependency Works in .NET 8

Dec 6, 2023 | .NET

The tech world never falls short on amazement, does it? Just as we were getting comfortable with existing features, .NET 8 sprang a welcome surprise our way. Keyed service dependency, a concept prevalent in other DI (Dependency Injection) frameworks like StructureMap and Autofac, is now part of the .NET 8 package. It’s like getting a new toy from your favorite toy set.

This article is your companion to navigate through the newly introduced functionality, its possible applications, and its potential to transform dependency handling in your .NET application.

Discovering Keyed Services

Imagine, finding a secret door in your existing toolkit that opens up an entire new room for you to explore. That’s similar to what .NET 8 introduces with Keyed Services in Dependency Injection.

A “keyed” or “named” registration is like adding an ID tag to your service. A service is not only related to its type but also an additional key. This lets you pinpoint to a specific implementation if there are multiple classes registered for an interface. It’s like having a bunch of keys, and each one opens a different lock.

Let’s dive into an example to see it functioning in an application.

// Declare an interface
public interface ITask {}
// Create two classes implementing the interface
public class TaskA: ITask {}
public class TaskB: ITask {}

// Register the classes with a unique key (like adding a tag)
container.Register<ITask, TaskA>("tagA");
container.Register<ITask, TaskB>("tagB");

// You can then get the exact implementation using the key (Think of using the correct key from your key bunch)
var myTaskA = container.Resolve<ITask>("tagA"); // This will fetch TaskA 
var myTaskB = container.Resolve<ITask>("tagB"); // This will fetch TaskB

Now that you’ve got a teaser, let’s go ahead and unveil the full story.

Exploring the Practical Uses of Keyed Service in .NET 8

Have you ever unwrapped a gift just to find it wasn’t quite what you needed right now? A lot like that, you may find the innovative concept of keyed services seemingly unnecessary. However, like any good surprise, it’s about exploring the potential it has that can make it a game-changer. Let me show you how.

A/B Testing and Feature Toggles made simple

Imagine you’re playing catch with your twin sibling. Your eyes are shut, and you don’t know who will throw the ball, A or B? This unknown, exciting element is exactly what can be induced in your methods using keyed services. This technique especially plays an important role when we’re doing A/B testing or working on feature toggles.

Allow me to explain this with the help of an easy-to-understand code example:

// In .NET startup file, we register two different flavors of IGameService, namely GameAService and GameBService using keys 1 and 2 respectively.
builder.Services.AddKeyedTransient<IGameService, GameAService>(1);
builder.Services.AddKeyedTransient<IGameService, GameBService>(2);

// Decide which service to provide (similar to deciding who will throw the ball in the catch game)
builder.Services.AddTransient<IGameService>(serviceProvider => {
    // Using the Random() class to generate a random key (1 or 2)
    var randomKey = new Random().Next(1, 3);
    // Pass this key to fetch the required service
    return serviceProvider.GetRequiredKeyedService<IGameService>(randomKey);
});

/* Now, we create an interface IGameService and two classes GameAService and GameBService implementing this interface. GameAService and GameBService are like the two versions A and B in A/B testing.*/
public interface IGameService{
    string PlayGame();
}
public class GameAService : IGameService{
    public string PlayGame(){
        // This game version returns "A"
        return "Playing game A!";
    }
}
public class GameBService : IGameService{
    public string PlayGame(){
        // This game version returns "B"
        return "Playing game B!";
    }
}

With this setup, we can enjoy the thrill of unpredictability each time we decide to “PlayGame”. Depending on the random key generated, we either get to play Game A or B, just like not knowing who will throw the ball in our catch game.

Managing Configurations? No Problem!

Thinking of configurations, did you ever play with those colorful stackable rings as a kid? Each ring is different and fits in its place perfectly, right? Kinda similar to how we handle configurations in different environments of our application.

Check out this fun piece of code to understand it:

// Register the "real" PizzaMaker for live environments
builder.Services.AddKeyedTransient<IPizzaMaker, RealPizzaMaker>("live");

// Register the "imaginary" PizzaMaker for non-live environments
builder.Services.AddKeyedTransient<IPizzaMaker, ImaginaryPizzaMaker>("not-live");

// Now let's decide which PizzaMaker service to fetch based on the environment
builder.Services.AddTransient<IPizzaMaker>(serviceProvider => {
    var env = serviceProvider.GetRequiredService<IHostingEnvironment>();
    var key = env.IsDevelopment() ? "not-live" : "live";
    return serviceProvider.GetRequiredKeyedService<IPizzaMaker>(key);
});

public interface IPizzaMaker{
    string MakePizza();
}
public class RealPizzaMaker : IPizzaMaker{
    public string MakePizza(){
        // Real pizza for real, live environments!
        return "Here's the real pizza. Enjoy!";
    }
}
public class ImaginaryPizzaMaker : IPizzaMaker{
    public string MakePizza(){
        // Imaginary pizza to play around in non-live environments!
        return "Well, here's your imaginary pizza for testing!";
    }
}

It’s just like using different stackable rings (configurations) when playing in different environments (playrooms). With this, we know what configuration or service to use depending on our environment just like knowing which ring fits better on which rod.

Potential Downsides

While keyed services are like an oasis in the desert, stepping in haphazardly may land you in quicksand. Here are a few aspects you should bear in mind before you jump in and start refactoring your DI setup.

Complex configuration, runtime errors, lack of type safety and other complications could arise if not appropriately managed.

Complex Configuration

One thing to note is that while this may seem like an excellent addition on the surface, if your team members or new developers aren’t familiar with this approach, they may find themselves in the deep, murky waters of complex dependency configuration. However, the benefits and potential use-cases, as mentioned above, can serve as light-houses guiding them towards safe coasts.

Be Cautious of Slip-Ups (Runtime Errors)

Just like how spilling that red punch over your white rug could result in a catastrophe, wrong configurations can trigger errors in your system during runtime. Imagine playing a video game. Your character is about to fight the final boss, and suddenly the game freezes. How would you feel? Frustrated right? This freeze is a “runtime error.” Now let’s translate this to coding with .NET.

// Let's register services
container.Register<IService, ServiceA>("keyA");
container.Register<IService, ServiceB>("keyB");

// Oops! We used an incorrect key
var myServiceC = container.Resolve<IService>("keyC"); // This line will cause a runtime error.

In this code, we registered ServiceA and ServiceB with their respective keys. But, later, we tried to access ServiceC using "keyC", which is not registered. This action will unleash an error when the program is running, just like when our game got frozen.

Keeping the Keys Safe (Lack of Type Safety)

Keyed services majorly depend on, you guessed it, keys! It’s as if your house key and car key hold the same value. So if you swap them by mistake, you can’t open your car with your house key, right? Your keys must match the locks they’re meant to open. Similar logic applies when handling keys in .NET.

// Here, we are registering our services
container.Register<IService, ServiceA>("ServiceA");
container.Register<IService, ServiceB>("ServiceB");

// Oh no! We misspelled the key
var service = container.Resolve<IService>("ServieA"); 

In this code, we put an incorrect key "ServieA" instead of "ServiceA". This minor spelling mistake is like trying to unlock the car with the house key; it just won’t work.

Avoid Overdoing it (Overuse or Misuse)

It’s never a good idea to use a single solution for all your problems. Just like you can’t solve all your math homework using only addition, you can’t use keyed services to solve all coding issues. Otherwise, your code might become as messy as that one time your 8-year-old brother tried to make cake covered in ketchup, relish, and chocolate syrup.

// Registering different functionalities under the same interface
container.register<IHandler, OrderSaver>("SaveOrderHandler");
container.register<IHandler, VatProcessor>("VatExcludedOrderProcessor");

// Trying to resolve dependencies
var saver = container.Resolve<IHandler>("SaveOrderHandler");
var processor = container.Resolve<IHandler>("VatExcludedOrderProcessor");

In this example, we used IHandler as a catch-all interface, with services being distinguished only by keys. While it may seem like a handy toolbox, excessive dependency on this could lead your code into becoming a jumbled mess. The same way chocolate spaghetti might sound cool at first to an 8-year-old, but in reality, it’s probably not a great idea.

Performance Overhead

Using keyed services could also have performance implications. Dependency resolution at runtime could introduce performance overhead, especially in scenarios where numerous dependencies exist.

But the final verdict is – Should you embrace this fantastic new addition? Yes, give it a try but remember, with great power comes great responsibility!

So, get your developer hat on and start exploring.

You May Also Like

Sign up For Our Newsletter

Weekly .NET Capsules: Short reads for busy devs.

  • NLatest .NET tips and tricks
  • NQuick 5-minute reads
  • NPractical code snippets
.