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.

To download the source code for this article, you can visit our GitHub repository.

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:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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:

app settings visual studio properties

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!