It’s all in the Host Class – Dependency Injection with .NET

I’ve written several articles about dependency injection with .NET Core. With changes since .NET Core 3, an update is necessary. This is an article of a services taking advantage of the Host class.

In the first article of this series, dependency injection is introduced, and I’m showing how a dependency injection container can be created with the Host class.

Breaking Chains

Without Dependency Injection

Let’s start with a small sample where dependency injection is not used. Here, the GreetingService class offers a simple Greet method to return a string:

public class GreetingService
{
    public string Greet(string name) => $"Hello, {name}";
}

This GreetingService class is used from the Action method in the HelloController:

public class HelloController
{
    public string Action(string name)
    {
        var service = new GreetingService();
        string message = service.Greet(name);
        return message.ToUpper();
    }
}

Finally, the Main method of the Program class instantiates the HelloController and invokes the Action method.

class Program
{
    static void Main()
    {
        var controller = new HelloController();
        string result = controller.Action("Stephanie");
        Console.WriteLine(result);
    }
}

The application runs writing a greeting message to the console. What’s the issue with this implementation? The HelloController has a string dependency on the GreetingService class. If the implementation of the GreetingService should be changed, e.g. offering one impementation where a database is accessed, or a REST service invoked, the HelloController needs to be changed as well. Also, creating a unit test for the Actionmethod of the HelloController, the test shouldn’t cover the GreetingService. With a unit test of the Action method we only want to test the implementation of this method, and no other dependencies. Let’s solve this in the next step.

Inversion of Control – With Dependency Injection

Inversion of control is a design principle where – as the name says – the one who has control is inverted. Instead of a method (typically one within a library) that defines the complete functionality on its own, the caller can supply code. This code in turn is invoked by the called method.

Using .NET, inversion of control can be implemented by using delegates or interfaces. To allow the HelloController not to take a dependency of the GreetingService class, the IGreetingService interface is introduced. This interface defines all the requirements for the HelloController, the Greet method:

public interface IGreetingService
{
    string Greet(string name);
}

The GreetingService class implements this interface:

public class GreetingService : IGreetingService
{
    public string Greet(string name) => $"Hello, {name}";
}

Now the HelloController class can be changed to take a dependency only on the interface IGreetingService. This interface is injected in the constructor of the HelloController.

public class HelloController
{
    private readonly IGreetingService _greetingService;
    public HelloController(IGreetingService greetingService)
    => _greetingService = greetingService;

    public string Action(string name)
    {
        string message = _greetingService.Greet(name);
        return message.ToUpper();
    }
}

The class to be used for the interface IGreetingService now needs to be defined outside of the HelloController – passing an object implementing the interface IGreetingService on instantiating of the object HelloController. This is where the term inversion of control is used: The control what type is used is now passed to the outside.

Inversion of control is also known by the name “Hollywood principle” – don’t call us, we’ll call you.

The Program class is now changed to pass a instance of the GreetingService to the constructor of the HelloController. Other than that, the implementation does not differ:

class Program
{
    static void Main()
    {
        var controller = new HelloController(new GreetingService());
        string result = controller.Action("Matthias");
        Console.WriteLine(result);
    }
}

With dependency injection, a dependency such as a type implementing the IGreetingService is injected.

Using a Container

The issue with injecting dependencies, is that it’s usually not that simple. As the application grows, it’s not just one object that needs to be passed to the constructor. And sometimes state need to be kept, and probably the same instance of one service needs to be passed to multiple controllers or services. To reduce the complexity, a dependency injection container can be used. With .NET, you can use the NuGet package Microsoft.Extensions.DependencyInjection. This DI container is used with ASP.NET Core and EF Core. This container can also be used with UWP, Xamarin, and WPF.

With the next change, the NuGet package Microsoft.Extensions.DependencyInjection is added. In the GetContainer method, the services that should be injected are added to the service ecollection. The ServiceCollection class keeps a list of the services. On invoking the BuildServiceProvider method, a ServiceProvider object that can be used to access the registered services is returned. The greeting service is registered passing the contract and the implementation types to the generic parameters of the AddTransient method. With the constructor of the HelloController class, the contract type is used for injection. The HelloController class itself doesn’t implement an interface, and this type will be retrieved from the DI container.

In the Main method, the HelloController is retrieved from the DI container using the GetService method. The constructor of the HelloController requires an object implementing IGreetingService. Because the DI container knows this type, it can create a new instancde of the HelloController and pass an instance of the GreetingServicetype. In case the DI container does not know the type it needs, it throws an exception of type InvalidOperationException with the information unable to resolve service for type IGreetingService while attempting to activate HelloController. With this message it’s clear that the IGreetingService interface is not registered with the DI container.

class Program
{
    static void Main()
    {
        using var container = GetContainer();
        var controller = container.GetService<HelloController>();
        string result = controller.Action("Katharina");
        Console.WriteLine(result);
    }

    static ServiceProvider GetContainer()
    {
        var services = new ServiceCollection();
        services.AddTransient<IGreetingService, GreetingService>();
        services.AddTransient<HelloController>();
        return services.BuildServiceProvider();
    }
}

Several extension methods can be used to register services: AddSingleton, AddTransient, and AddScoped. Registering a service into the DI container with the method AddTransient returns a new object every time the type is injected. With the method AddSingleton, the same object is returned with every injection. The method AddScoped is somewhere in between. In the same scope, the same instance is returned. In a different scope, a new instance is created. What is a scope? With ASP.NET Core applications, a HTTP request creates a new scope. Injecting services based on the HTTP request, with scoped services the same object is returned.

Using Parameters

In case a service needs some parameters, you cannot define a constructor with just the types you need. The DI container doesn’t know how to pass these parameters. However, you can use a type that is known by the DI container to instantiate – e.g. a custom interface that is registered with the DI container. Instead of creating a custom interface, you can make use of the IOptions interface. This is a generic interface that allows passing a type, such as the GreetingServiceOptions class. This class is used to specify the values needed for the new GreetingService class:

public class GreetingServiceOptions
{
    public string From { get; set; } = string.Empty;
}

The GreetingService class now has a constructor that receives IOptions.

public class GreetingService : IGreetingService
{
    private readonly string _from;
    public GreetingService(IOptions<GreetingServiceOptions> options)
        => _from = options.Value.From;

    public string Greet(string name) 
        => $"Hello, {name}, greetings from {_from}";
}

To remove the requirement to invoke the AddTransient method and to configure the service calling the Configure method, the extension method AddGreetingService is defined. This method extends the interfacde IServcieCollection, and thus can be used on configuration of the DI container. In the implementation, the Configure method is invoked passing a method that returns the type to specify the configuration for the service: GreetingServiceOptions. After the configuration is done, the service is registered to the DI container with the AddTransient method:

public static class GreetingServiceCollectionExtensions
{
    public static IServiceCollection AddGreetingServce(this IServiceCollection services,
        Action<GreetingServiceOptions>? setupAction = default)
    {
        if (setupAction != null)
        {
            services.Configure(setupAction);
        }
        return services.AddTransient<IGreetingService, GreetingService>();
    }
}

The configuration of the DI container can now be adapted to invoke the method AddGreetingsService, and to pass a method returning GreetingServiceOptions.

class Program
{
    static void Main(string[] args)
    {
        using var host = Host.CreateDefaultBuilder()
            .ConfigureServices(services =>
            {
                services.AddGreetingServce(options =>
                {
                    options.From = "Christian";
                });
                services.AddTransient<HelloController>();
            })
            .Build();
        var controller = host.Services.GetService<HelloController>();
        string result = controller.Action("Matthias");
        Console.WriteLine(result);
    }
}

There’s also an extension method AddOptions to register an implementation of the IOptions interface with the DI container, so the container knows how to create instances of types that need this interface with construction. Using the CreateDefaultBuilder of the host factory to create a Host instance, this is not needed, as this method already specifies a few interfaces that are commonly needed with the DI container, e.g. for logging and configuration. You can already run the application and see the results.

Take away

Dependency injection reduces strong dependencies. Instead of having strong references to concrete types, constructor injection can be used to pass the concrete implementations from the outside. A dependency injection container becomes handsome when multiple services probably with different lifetimes need to be configured with the container. The configuration of the Host class already has a DI container built-in, and registers some commonly used services. If you add a breakpoint to the invocation of the ConfigureServices method, you’ll see that already 31 services are registered befor the call to AddGreetingService. Among these are IHostEnvironment, IConfiguration, IApplicationLifetime, ILogger, and others.

Expect to read more about the Host class in future articles.

If you’ve read this far, consider buying me a coffee which helps me staying up longer and writing more articles.

Buy Me A Coffee

You can get the complete sample code.

Enjoy learning and programming!

Christian

More Information

Azure Functions with Dependency Injection

Disposing Injected Services

HTTP Client Factory with .NET Core

More information on C# and programming .NET Core applications is in my book Professional C# 7 and .NET Core 2.0, and in my workshops.

ID 143279026 © Rodolphe Trider | Dreamstime.com
Breaking Chains ID 143279026 © Rodolphe Trider | Dreamstime.com

12 thoughts on “It’s all in the Host Class – Dependency Injection with .NET

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.