.NET DI: Beyond the Basics
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.
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.
// 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:
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();
}
}
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:
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.
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();
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!
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!");
}
}
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:
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!
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"));
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.
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();
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();
}
}
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.
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!