Introduction
In this article, we’ll look at ways to use dependency injection with constructor parameters in .NET Core. Dependency injection (DI) with constructor parameters is a way for us to pass the things our class needs (depends on) when we create a new instance of it.
Let’s start by adding dependency injection to our application.
How to Add Dependency Injection to a Simple .Net Core App
The first thing we need to do with our application is to give it the ability to use DI:
dotnet add package Microsoft.Extensions.Hosting
The Microsoft.Extensions.Hosting
NuGet package provides us with some convenient extension methods which include the setup of DI.
Let’s add the code to make use of this NuGet package:
var host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { // We'll register our services to the DI // container here }) .Build();
We use the Host.CreateDefaultBuilder()
method provided to us by the Microsoft.Extensions.Hosting
NuGet package to build our IHost
object. An IHost
object is an abstraction of our program and it’s what we use to set the program up and run it.
Let’s create a service that we can register in the DI container.
Create an Example Service
The service we make will write animal sounds from a list to the console:
public class AnimalSoundService : IAnimalSoundService { private readonly List<string> AnimalSounds; public AnimalSoundService() { AnimalSounds = new List<string>() { // We need to add some sounds here }; } public void Play() { foreach (var sound in AnimalSounds) { Console.WriteLine(sound); } } }
We have the AnimalSounds
property in our class, which holds a list of strings – this will contain animal sounds that we pass through the constructor. The service also contains a Play()
method that iterates over the AnimalSounds
list and writes each sound to the console.
Let’s update the code in the Program
class to register the service and run it from DI:
var host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { services.AddScoped<IAnimalSoundService, AnimalSoundService>(); }) .Build(); var service = host.Services.GetRequiredService<IAnimalSoundService>(); service.Play();
We register our IAnimalSoundService
interface into our DI container and ask the container to give us an instance of the AnimalSoundService
class whenever we request an IAnimalSoundService
.
Finally, we ask the DI container for an instance of an IAnimalSoundService
, store the result into the service
variable, and then call the service’s Play()
method.
The AnimalSounds
list is empty right now so let’s add a sound to it.
How to Pass a Service to the Constructor
The first sound we add to our list will come from a different service:
public class DogSoundService : IDogSoundService { public string GetSound() { return "Woof"; } }
The DogSoundService
class has 1 method that returns the string, "Woof"
.
Let’s register the service into the DI container:
var host = Host.CreateDefaultBuilder() .ConfigureServices(services => { services.AddScoped<IDogSoundService, DogSoundService>(); services.AddScoped<IAnimalSoundService, AnimalSoundService>(); }) .Build();
Now whenever we ask for an IDogSoundService
in a constructor, the DI container will provide us with a DogSoundService
instance.
The AnimalSoundService
constructor can be updated to request an IDogSoundService
instance and then we can add the result of the IDogSoundService.GetSound()
method to the AnimalSounds
list:
public AnimalSoundService(IDogSoundService dogSoundService) { AnimalSounds = new List<string>() { dogSoundService.GetSound() }; }
The output when we run the application is:
Woof
Let’s use the configuration to get the next sound for the AnimalSounds
list.
How to Pass Configuration to the Constructor
Configuration in .NET can be done in a couple of different ways. We can use the IConfiguration
interface directly in classes or the preferred option which is to use the options pattern to provide a strongly typed section of our configuration using the IOptions<T>
interface.
How to Inject IConfiguration
When we set the application up, we used the Host.CreateDefaultBuilder()
method which conveniently registers the .NET IConfiguration object into the DI container and loads the app configuration from some providers including the appsettings.json
file.
Let’s create the appsettings.json
file and add a setting for a cat sound:
{ "CatSound": "Meow" }
The appsettings.json
properties need to be set so that the “Copy to Output Directory” value is “Copy if newer” or “Copy always”. This will ensure that the latest version of the file is included when we run our app:
Let’s update the AnimalSoundService
class to inject the IConfiguration
:
public AnimalSoundService(IDogSoundService dogSoundService, IConfiguration configuration) { AnimalSounds = new List<string>() { dogSoundService.GetSound(), configuration["CatSound"], }; }
We inject the IConfiguration
object and add the "CatSound"
setting from the configuration into the AnimalSounds
list.
The output when we run the app is:
Woof Meow
Let’s add another sound to the app configuration and get it using the options pattern.
How to Inject IOptions<T>
The IOptions<T>
interface is a generic interface that takes a type to bind our configuration section to.
Let’s create a new class to bind a new animal sound setting to:
public class CowOptions { public string CowSound { get; set; } }
The CowOptions
class has one property called CowSound
. When we bind our configuration section to this class, it will set the class property values to the value of the configuration setting with the same name.
We don’t have a "CowSound"
setting in our configuration yet so let’s add a section for it to our appsettings.json
file:
{ "CatSound": "Meow", "CowSettings": { "CowSound": "Moo" } }
Let’s update the ConfigureServices
extension method to bind our "CowSettings"
configuration section to our CowOptions
class:
var host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { services.Configure<CowOptions>(context.Configuration.GetSection("CowSettings")); services.AddScoped<IDogSoundService, DogSoundService>(); services.AddScoped<IAnimalSoundService, AnimalSoundService>(); }) .Build();
We can inject IOptions<CowOptions>
into the AnimalSoundService
constructor. Doing this will give us strongly-typed access to the configuration settings:
public AnimalSoundService(IDogSoundService dogSoundService, IConfiguration configuration, IOptions<CowOptions> options) { AnimalSounds = new List<string>() { dogSoundService.GetSound(), configuration["CatSound"], options.Value.CowSound }; }
The output when we run the application:
Woof Meow Moo
Up til now, the injected constructor parameters have all been types that are registered in the DI container. Let’s add one more parameter to the AnimalSoundService
constructor that’s not registered in the DI container.
How to Pass Parameters That Are Not in the DI Container to the Constructor
Let’s start by updating the AnimalSoundService
constructor to take one more parameter that we can add to the AnimalSounds
list:
public AnimalSoundService(IDogSoundService dogSoundService, IConfiguration configuration, IOptions<CowOptions> options, string sheepSound) { AnimalSounds = new List<string>() { dogSoundService.GetSound(), configuration["CatSound"], options.Value.CowSound, sheepSound }; }
If we try to run the application now, we get an exception because the DI container doesn’t know how to resolve the new string parameter we added to the constructor.
We can fix this issue by telling the DI container how to resolve a string:
services.AddScoped<string>(_ => "baa");
When we register a string in the DI container like this, it will mean that every string we inject into every class will be resolved as "baa"
. It’s not advisable to register a simple type like this.
A more suitable way to register the string for the AnimalSoundService
class is to change the way we register it and use an overloaded version of the AddScoped()
method:
services.AddScoped<IAnimalSoundService, AnimalSoundService>( serviceProvider => new AnimalSoundService( dogSoundService: serviceProvider.GetRequiredService<IDogSoundService>(), configuration: serviceProvider.GetRequiredService<IConfiguration>(), options: serviceProvider.GetRequiredService<IOptions<CowOptions>>(), sheepSound: "Baa") );
The overloaded version of the AddScoped()
method allows us to provide a factory to create a new instance of the AnimalSoundService
. For the first three parameters in our AnimalSoundService
constructor, we use the DI container to get the dependency implementations. We provide the final string parameter in-line.
Let’s see what the output is when we run the application:
Woof Meow Moo Baa
Which Method Should You Use?
We looked at how we can inject different types of dependencies into a class. Some of the dependencies required us to register them differently from the rest, and others required us to change how we register the class that uses the dependencies.
There is no single right way to inject dependencies but there are some good practices to be aware of like:
- Inject interfaces, not implementations
- Use the options pattern to read related configuration settings
- Don’t register simple types (like string, int, char, etc.) into the DI container
- Make use of the factory overload method when registering a dependency if it’s required
Conclusion
In this article, we’ve looked at ways to use DI for different types of constructor parameters. We’ve looked at how we can utilize the DI container to register and inject different types of parameters suitable for a range of scenarios. Finally, we’ve looked at some good practices to consider when using DI.
Using the Options pattern as you did now means your AnimalSoundsService has to take a dependency on IOptions and hence pollutes it’s parameters list and minimizes the other scenarios that service could be used.
It would be better to just use the CowOptions directly in your AnimalSoundsService constructor instead of IOptions<CowOptions>. You would then need to change the lambda in the services.AddScoped call to use the Value property of the result of GetRequiredService<IOptions<CowOptions>>() to pass the CowOptions object into the constructor.
The same applies to using IConfiguration.
It’s a better separation of concerns to require the hosting environment (ie. ASP.NET in this case) to know how to pull information out of its configuration system and stuff it into CowOptions versus making your AnimalSoundsService tightly coupled to that particular configuration environment.
Hi Larry. Thank you for the comment. To be honest, I always used the IOptions pattern to extract configuration values and that would always be my first recommendation. This is exactly what we stated here if you want to extract values from the configuration, which is a quite common case. You can do your way as well, of course. Developers should choose what fits the best for their use case. Your comment is here, so if anyone likes that idea, they can use it.
I think you misunderstood my point, sorry if I wasn’t clearer…but I’m not arguing against using the IOptions pattern at all, I’m merely saying your AnimalSoundService class should not have to have any knowledge of (ie dependency on) IOptions, as it is a detail of your host model’s configuration capabilities and not of the AnimalSoundService.
Here your service’s constructor takes an IOptions<CowOptions> parameter, so your service needs to take a dependency on IOptions:
public AnimalSoundService(IDogSoundService dogSoundService,
IConfiguration configuration,
IOptions<CowOptions> options)
{
AnimalSounds = new List<string>()
{
dogSoundService.GetSound(),
configuration[“CatSound”],
options.Value.CowSound
};
}
And here is how your DI container instantiates your service and passes in configuration via IOptions<CowOptions>:
services.AddScoped<IAnimalSoundService, AnimalSoundService>(
serviceProvider => new AnimalSoundService(
dogSoundService: serviceProvider.GetRequiredService<IDogSoundService>(),
configuration: serviceProvider.GetRequiredService<IConfiguration>(),
options: serviceProvider.GetRequiredService<IOptions<CowOptions>>(),
sheepSound: “Baa”)
);
My point is that you should remove the IOptions dependency from your service by changing the constructor to accept the “raw” CowOptions class:
public AnimalSoundService(IDogSoundService dogSoundService,
IConfiguration configuration,
CowOptions options)
{
AnimalSounds = new List<string>()
{
dogSoundService.GetSound(),
configuration[“CatSound”],
options.CowSound
};
}
but you can still use the IOptions pattern and get the ease of configuration it provides by changing the AddScoped method’s lambda to use the Value property of the result of the serviceProvider.GetRequiredService<IOptions<CowOptions>>() call to pass the actual CowOptions object to the service’s constructor:
services.AddScoped<IAnimalSoundService, AnimalSoundService>(
serviceProvider => new AnimalSoundService(
dogSoundService: serviceProvider.GetRequiredService<IDogSoundService>(),
configuration: serviceProvider.GetRequiredService<IConfiguration>(),
options: serviceProvider.GetRequiredService<IOptions<CowOptions>>().Value,
sheepSound: “Baa”)
);
Now that you’ve removed the IOptions dependency from the AnimalSoundService class by replacing IOptions<CowOptions> with just CowOptions, the service can be used in any environment where you may not have the same IOptions functionality available
Oh, thank you. I got it now. Thank you for the clarification and the example.
That last version ( using the factory with all the GetRequiredServices ) can be accomplished using ActivatorUtilities.CreateInstance. It will resolve the remaining parameters not supplied. However caution needs to be advised here as you could inject Transient instances into Scoped which would normally be disallowed.
Thank you Rob for the comment. Also, if you have time, any example would be appreciated (just to be sure that we are on the same page), and we can even link it to the article.