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.
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 Action
method 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 GreetingService
type. 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
, andAddScoped
. Registering a service into the DI container with the methodAddTransient
returns a new object every time the type is injected. With the methodAddSingleton
, the same object is returned with every injection. The methodAddScoped
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.
You can get the complete sample code.
Enjoy learning and programming!
Christian
More Information
Azure Functions with Dependency Injection
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
Very good article presented with a very clear mind. Never thought about using IOptions in this manner. thanks
LikeLiked by 1 person
I have reading about DI with Option three time, I still don’t catch it.
LikeLike
Liuzu, can you be more specific on the questions you have? Why to use options? How to implement it? Why DI? ???
LikeLike
Hi, Isn’t it better to change ‘ services.AddTransient();’ to AddSingleton?
LikeLike
You should prefer transient over singleton. Use singleton only with services where state is shared.
You can’t inject a transient into a singleton registered object, but you can inject a singleton inside a transient registered object.
LikeLike