Skip to main content

.NET DI: Beyond the Basics

Β· 11 min read
Zsolt Zabolai
Software Engineer | Co-Founder @ BuggyDevs

In the earlier post, we talked about dependency injection in .NET.
Continuing on the same topic, there are a couple of additional features available in .NET DI we can explore.

This posts assumes that you're already familiar with the basic concepts of Dependency Injection in .NET and want to explore. In case you're not, the earlier post might be a better read beforehand.

note

By .NET DI, I'm referring to Microsoft's Dependency Injection library called Microsoft.Extensions.DependencyInjection.
It's not the only DI library in .NETβ€”there are other really good ones you can check out. πŸ˜‰

Keyed services​

In the previous post, we talked about pets... but it had a major flaw.
There are many pets in the world, each with their own name, personality, etc.
This is something we didn't handle very well.

Each owner wants to play with their own pet, not with someone else's. So, what do they do?
Of course, they call their pets by their names! What if we could do the same in .NET DI?

.NET 8 introduced a new feature called keyed services that does exactly what we want! If you're working on an actively maintained .NET project, it's very likely you're already on .NET 8 or later and can use this feature.

Keyed services allow us to define a key when registering a class in .NET DI. This key can then be used to reference and inject a specific implementation.
Each of the methods AddSingleton, AddScoped, and AddTransient has a keyed version: AddKeyedSingleton, AddKeyedScoped, and AddKeyedTransient, each of these takes a key parameter.

Example
// Add a Cat, as an implementation of IPet, with the key "Fluffy".
serviceCollection.AddKeyedSingleton<IPet, Cat>("Fluffy");

To choose a specific implementation by key, we can use the [FromKeyedServices] attribute:

CatOwner
class CatOwner
{
private readonly IPet _pet;

// Inject with the key "Fluffy"
public CatOwner([FromKeyedServices("Fluffy")] IPet pet)
{
_pet = pet;
}

public void PlayWithPet()
{
Console.WriteLine("CatOwner playing with pet!");
_pet.Talk();
}
}

Here's a complete example, with cats and dogs! 😺🐢

using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();

serviceCollection.AddKeyedSingleton<IPet, Cat>("Fluffy");
serviceCollection.AddKeyedSingleton<IPet, Dog>("Dog");
serviceCollection.AddSingleton<CatOwner>();
serviceCollection.AddSingleton<DogOwner>();

var serviceProvider = serviceCollection.BuildServiceProvider();

var catowner = serviceProvider.GetRequiredService<CatOwner>();
catowner.PlayWithPet();
var dogowner = serviceProvider.GetRequiredService<DogOwner>();
dogowner.PlayWithPet();

interface IPet
{
public void Talk();
}

class Cat : IPet
{
public void Talk()
{
Console.WriteLine("Miau!");
}
}

class Dog : IPet
{
public void Talk()
{
Console.WriteLine("Woooof!");
}
}

class CatOwner
{
private readonly IPet _pet;

public CatOwner([FromKeyedServices("Fluffy")] IPet pet)
{
_pet = pet;
}

public void PlayWithPet()
{
Console.WriteLine("CatOwner playing with pet!");
_pet.Talk();
}
}

class DogOwner
{
private readonly IPet _pet;

public DogOwner([FromKeyedServices("Dog")] IPet pet)
{
_pet = pet;
}

public void PlayWithPet()
{
Console.WriteLine("DogOwner playing with pet!");
_pet.Talk();
}
}
Output
CatOwner playing with pet!
Miau!
DogOwner playing with pet!
Woooof!

In this example, the class CatOwner asked for an IPet with the key "Fluffy", and got a Cat, while DogOwner asked for an IPet with the key "Dog" and got a Dog. (What a cute name for the dog: "Dog"! Columbo πŸ•΅οΈβ€β™‚οΈ would be proud!)

Keys can be anything​

An interesting detail about keys is that they don't have to be strings. They can be any kind of object. For example, we can use an enum:

Keys enum
enum Keys
{
Fluffy,
Dog
}
serviceCollection.AddKeyedSingleton<IPet, Cat>(Keys.Fluffy);
serviceCollection.AddKeyedSingleton<IPet, Dog>(Keys.Dog);

We can also use objects:

var dogKey = new object();
var catKey = new object();

serviceCollection.AddKeyedSingleton<IPet, Cat>(catKey);
serviceCollection.AddKeyedSingleton<IPet, Dog>(dogKey);

However, using objects has a limitation: we can't use objects in attributes. In this case, we can still retrieve the services using the ServiceProvider, but not through attribute-based injection.

Objects as keys
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();

var dogKey = new object();
var catKey = new object();

serviceCollection.AddKeyedSingleton<IPet, Cat>(catKey);
serviceCollection.AddKeyedSingleton<IPet, Dog>(dogKey);

var serviceProvider = serviceCollection.BuildServiceProvider();

var cat = serviceProvider.GetRequiredKeyedService<IPet>(catKey);
cat.Talk();
var dog = serviceProvider.GetRequiredKeyedService<IPet>(dogKey);
dog.Talk();
Output
Miau!
Woooof!

Injecting the key​

It turns out dogs and cats are smart. My dog constantly surprises me with how smart he is.
One of the first things an owner teaches them is their name, they need to listen to it and pay attention.

Fortunately, we can also model this in .NET DI! Whenever one of our classes is registered as a keyed service, it can receive its key as a constructor parameter by adding the [ServiceKey] attribute.

Here's an example with a cat!

Injecting the key
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedSingleton<IPet, Cat>("Fluffy");

var serviceProvider = serviceCollection.BuildServiceProvider();

var cat = serviceProvider.GetRequiredKeyedService<IPet>("Fluffy");
cat.Talk();

interface IPet
{
public void Talk();
}

class Cat : IPet
{
private readonly string _key;

// [ServiceKey] will inject the key of the current service. In our case: "Fluffy".
public Cat([ServiceKey] string key)
{
_key = key;
}

public void Talk()
{
Console.WriteLine($"{_key}: Miau!");
}
}
Output
Fluffy: Miau!

Now that Fluffy knows his own name, we can go further into .NET DI features...

Factory functions​

Let's take a step back from keyed services and name our pets without them. To start, we can define the Dog class like this:

class Dog : IPet
{
private readonly string _name;

public Dog(string name)
{
_name = name;
}

public void Talk()
{
Console.WriteLine($"{_name}: Wooof!");
}
}

And then our owner class:

class Owner(IPet pet)
{
public void PlayWithPet()
{
Console.WriteLine("Playing with pet!");
pet.Talk();
}
}

I used a primary constructor just to keep the example short. Click the link if you want to read more about them! πŸ˜‰

To complete our example, here's the main part of the program:

Program.cs
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPet, Dog>();
serviceCollection.AddSingleton<Owner>();

var serviceProvider = serviceCollection.BuildServiceProvider();

var owner = serviceProvider.GetRequiredService<Owner>();
owner.PlayWithPet();

What do we get out of this? A big exception!

Error
Unhandled exception. System.InvalidOperationException: Unable to resolve service for type 'System.String' while attempting to activate 'Dog'.

What does this mean? .NET DI tried to create an instance of Dog for us but couldn't. It's saying that it tried to find a service to pass to the Dog constructor, but there was nothing in the ServiceCollection it could use...

So, what can we do? One way to fix this problem is to register our Dog with a factory function!

serviceCollection.AddSingleton<IPet, Dog>((IServiceProvider serviceProvider) => new Dog("Dog"));
// We can also omit the type:
// serviceCollection.AddSingleton<IPet, Dog>((services) => new Dog("Dog"));
Output
Playing with pet!
Dog: Wooof!

The Add methods of the ServiceCollection allow us to pass an anonymous function that returns an instance of the registered class. This lets us call the constructor of the Dog class ourselves and pass in any required parameters. It can be useful in scenarios where constructor parameters can't be resolved automatically by .NET DI.
It's a sort of escape hatch for situations that .NET DI can't handle on its own.

ActivatorUtilities​

Notice in the example above, we get an IServiceProvider as a parameter to our factory function. What if our Dog class also has other dependencies? Those would appear as constructor parameters, right? Does that mean that in the factory function we need to pass all of them?

Well, .NET DI also has a solution for this problem, in a class called ActivatorUtilities.

To illustrate this, we need to add a dependency of our Dog class.... maybe some dog food?

class Dog(DogFood dogFood, string name) : IPet
{
public void Talk()
{
Console.WriteLine($"{name}: Wooof! I eat {dogFood}");
}
}

// Dog food doesn't do much... it just gets eaten...
class DogFood
{
public override string ToString() => "SuperTastyDogFood";
}

Now, how do we do this in our factory function? We still want to pass in the name manually, but we also want to avoid pasing in the DogFood... that should be handled by .NET DI.
The class ActivatorUtilities has some pretty handy methods for this. In our case we can use CreateInstance. It accepts an IServiceProvider as a parameter, and any additional parameters we want to pass into the class. It will use the parameters that we provide, while resolving the rest from the service provider.

Program.cs
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPet, Dog>((serviceProvider) => ActivatorUtilities.CreateInstance<Dog>(serviceProvider, "Sparky"));
serviceCollection.AddSingleton<Owner>();
serviceCollection.AddTransient<DogFood>();

var serviceProvider = serviceCollection.BuildServiceProvider();

var owner = serviceProvider.GetRequiredService<Owner>();
owner.PlayWithPet();
Output
Playing with pet!
Sparky: Wooof! I eat SuperTastyDogFood

To be honest, ActivatorUtilities is not something you're going to use every day...

Some more exotic .NET features are implemented by using it, for example Typed HttpClients.
In application code it is rarely used, but if you're writing a library it could come in handy. Perhaps in one of the next posts we could do just that. πŸ˜‰

Injecting an IServiceProvider​

In the factory function we got an IServiceProvider as a parameter... one might wonder, is it possible to inject that into any service?
The short answer is: Of course! Admitedly this is also rarely needed, but could be useful in certain scenarios.

As an example we can summon our dog just for a playing session this way, and let him go back to rest afterwards:

using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<Owner>();
serviceCollection.AddScoped<IPet, Dog>(s => new Dog("Sparky"));

var serviceProvider = serviceCollection.BuildServiceProvider();

var owner = serviceProvider.GetRequiredService<Owner>();
owner.PlayWithPet();

interface IPet
{
public void Talk();
}

class Dog(string name) : IPet
{
public void Talk()
{
Console.WriteLine($"{name}: Wooof!");
}
}

class Owner(IServiceProvider serviceProvider)
{
public void PlayWithPet()
{
// Owner is Singleton, so we can't inject the Scoped IPet in the constructor.
// What we can do: inject the IServiceProvider and create a new scope for a little playing session.
// This keeps everyone happy! Our dog can go back to sleep after. ;)
// BTW. Did you know that dogs sleep between 12 to 18 hours per day? Mine is for sure on the higher side πŸ˜…
using var scope = serviceProvider.CreateScope();
var pet = scope.ServiceProvider.GetRequiredService<IPet>();
Console.WriteLine("Playing with pet!");
pet.Talk();
}
}
Output
Playing with pet!
Sparky: Wooof!

This can be useful whenever we want to create a specific scope for an operation. Microsoft illustrates this approach in a BackgroundService to consume scoped dependencies.

Injecting a Lazy anything! πŸ¦₯​

Now that we know how much dogs sleep per day... we need to illustrate in our code that they can be lazy!
.NET has a Lazy class, which is used for lazy initialization. When creating certain objects takes lots of time, this class can be used to defer initialization.

In our case, since our dog sleeps so much, we'd better make him Lazy!
BTW. This is not my original idea, it comes from this StackOverflow answer.

Lazy
serviceCollection.AddTransient(typeof(Lazy<>), typeof(Lazier<>));

internal class Lazier<T> : Lazy<T> where T : class
{
public Lazier(IServiceProvider provider)
: base(() => provider.GetRequiredService<T>())
{
}
}

// After doing this, we can change our Owner class too:
class Owner(Lazy<IPet> lazyPet)
{
public void PlayWithPet()
{
var pet = lazyPet.Value;
Console.WriteLine("Playing with pet!");
pet.Talk();
}
}

While needing Lazy dependencies is also an edge case, and rarely needed, the example also illustrates one way .NET handles generic classes. In this example every time a Lazy<T> is injected, .NET DI resolves it into a Lazier<T>.

Using Lazy can be seen as a performance optimization in some cases, but it also comes with an overhead. It can be used to break circular dependencies too, but at that point something is probably wrong with the design. If you want to use it, you will need to look into a bit more to see if it's worth it for you. πŸ˜‰

Final words​

Today we dived a bit deeper into Dependency Injection in .NET. Some of the things might have been somewhat more exotic and rarely used during application development.
Still, I believe that it's good to know about them: even if we don't memorize all the API signatures and details, it can be useful to know that these features exist. (Also, memorizing all the details in a world with an AI coding asistent might not be needed at all anymore... who knows? πŸ˜…)

I hope you enjoyed it and see you next time!