Introduction to dependency injection - DI in .NET Core - Part 2
Orestis Meikopoulos
Head of Engineering | Cultivating Technical Leadership | C# & .NET Content Creator | Public Speaker
In Part 1 of this small series, we went over a small introduction around the concept and the theory behind dependency injection and why and how it is used to build loosely coupled, extensible and testable applications. In today's Part 2, we will examine how DI is being applied in ASP.NET Core applications.
Dependency injection is provided out of the box in ASP.NET Core web applications. You register all your dependencies to the .NET Core IoC service container at application startup and get them injected in the client code where and whenever required, mostly by using constructor injection like in the example in the below image (Index2Model class with IMyDependency being injected). ASP.NET Core provides a built-in service container called IServiceProvider. Services are registered in the app's Startup.ConfigureServices method.
With the use of DI pattern in the above example, the Index2Model class doesn't use the concrete type MyDependency, but only the interface IMyDependency. That makes it easy to change the implementation that the class uses without having to modify the Index2Model class itself. The Index2Model class doesn't create an instance of MyDependency, it's created by the ASP.NET Core built-in DI container.
Dependency injection can be used in a chained fashion. Each requested dependency in turn requests its own dependencies. The container resolves the dependencies in the graph and returns the fully resolved service. The collective set of dependencies that must be resolved is typically referred to as a dependency tree, dependency graph, or object graph.
The standard and ideal way in .NET Core to get a service instance injected in a class is to use Constructor Injection. You simply add the dependencies as parameter to the constructor of the class and then the .NET Core DI framework will inject the actual instances as arguments at runtime. An important thing to note here, though, is that the DI framework can only inject constructor dependencies when the class is instantiated through the use of the built-in DI container. If, for any reason, a new instance is created directly with the use of the new keyword or with Reflection, DI will play no role in the process. Constructor injection is the ideal way to inject dependencies in a class as it is conventional, very descriptive and readable. It also makes the intention very clear that the code will not function if those dependencies are not provided.
There can be cases, where one might need to get a service instance by explicit injection. This can be achieved by asking for an instance directly from IServiceProvider. This IServiceProvider itself can be injected through DI. Some cases for this would be, for example, to create service instances conditionally at runtime or to minimize the parameters of constructor. Only IServiceProvider can be injected in constructor, then later the actual services can be instantiated as and when necessary.
The framework also provides an out of the box robust logging system (among other framework-provided services as well). Most apps shouldn't need to write loggers. The following code demonstrates using the default logging, which doesn't require any services to be registered in ConfigureServices. Logging is provided by the .NET framework itself (more on logging in .NET Core in a future article - stay tuned).
In .NET Core there is also the ability to register services using extension methods. Related groups of registrations can be moved to an extension method to register related services that together offer some sort of specific functionality to our application. Each services.Add{SERVICE_NAME} extension method adds and potentially configures services. For example:
Each service that we register in ASP.NET Core applications comes with a specified lifetime. There are three different options here. Transient lifetime services (AddTransient) are created each time they're requested from the service container. This lifetime works best for lightweight, stateless services. In apps that process requests, transient services are disposed at the end of the request. Scoped lifetime services (AddScoped) are created once per client request (connection). In apps that process requests, scoped services are disposed at the end of the request. An important thing to note here is that, you should not resolve a scoped service from a singleton. It may cause the service to have incorrect state when processing subsequent requests. On the other hand, it's fine to:
Singleton lifetime services (AddSingleton) are created either the first time they're requested or by the developer, when providing an implementation instance directly to the container. Every subsequent request uses the same instance. If the app requires singleton behavior, allow the service container to manage the service's lifetime. Don't implement the singleton design pattern and provide code to dispose the singleton. In apps that process requests, singleton services are disposed when the ServiceProvider is disposed on application shutdown. Because memory is not released until the app is shut down, memory use with a singleton must be considered. Singleton services must also always be thread safe and are often used in stateless services.
Let's see some best practices when using DI in ASP.NET Core applications:
In .NET Core it is also possible to replace the default service container that is provided to us out of the box from the framework with a different third-party container implementation. However, it is generally recommended to use the built-in container unless you need a specific feature that the built-in container doesn't support, such as: Property injection, injection based on name, child containers, custom lifetime management, Func<T> support for lazy initialization and convention-based registration. The following third-party containers can be used with NET Core apps: Autofac, DryIoc, Grace, LightInject, Lamar, Stashbox and Unity.
Finally, we should also note that, initially, the IServiceCollection provided to ConfigureServices method (responsible for defining the services that the app uses, including platform features, such as Entity Framework Core and ASP.NET Core MVC) has services defined by the framework depending on how the host was configured. Apps based on an ASP.NET Core template have more than 250 services registered by the framework already for you, some of them you can find in the below image along with their registered lifetime.
Before closing this article and this small series on DI, I would like to show you a small example of how you can register services with the default ASP.NET Core DI container and how the scope lifetimes are used.
For this purpose, we will use a simple Razor Pages .NET Core application. At first, we are going to define an IOperation interface containing a simple getter that returns a string and is called "OperationId". Also, we are going to define three other interfaces called IOperationTransient, IOperationScoped and IOperationSingleton. Each of these three new interfaces are going to extend the IOperation interface (in order for all of three of them to have access to the OperationId getter property). We will use each of these three interfaces to register one service with the respective lifetime scope in the DI container. So IOperationTransient will be used to register one implementation of it with the Transient lifetime scope, the IOperationScoped with the Scope lifetime and finally the IOperationSingleton with the Singleton lifetime.
领英推荐
The four interface we mentioned above, will look like the following in code:
Next, we are going to define a simple implementation of the IOperationTransient, IOperationScoped and IOperationSingleton interfaces in a class called Operation. This implementation will implement all three of the interfaces and will need to provide an implementation for the OperationId getter property of type string. The OperationId in our example will have the value of the last 4 digits of a simple Guid value (a unique identifier in .NET):
Now, it is time to use the above classes to define our services in the .NET Core DI framework, so we will open the Startup.cs class and we are going to register the required services inside the ConfigureServices method, like below:
As you can see, we have registered a separate instance of the Operation class for each of the three interfaces we have defined in our codebase, along with their respective lifetime scopes (IOperationTransient using AddTransient, IOperationScoped using AddScoped and IOperationSingleton using AddSingleton). We can now go ahead and inject these three services using the IOperationTransient, IOperationScoped and IOperationSingleton interfaces, wherever we want in our codebase.
To be able to demonstrate how the different lifetime scopes are behaving, we are going to inject the above services in two places. Firstly, we are going to inject them inside the IndexModel razor page and secondly, we are going to create a middleware component and register it inside the Configure method of the Startup class, in order to configure the?HTTP request pipeline. As a result of this, our middleware component will get called before the execution reaches the IndexModel razor page codebase.
To see how the objects of the interfaces are being created based on their registered lifetime scope, we are going to simply log the OperationId value to the console.
Let's see what we have done in action. I will run the Razor Page application and will click inside the "Home" page of the menu. This will trigger the MyMiddleware class InvokeAsync method to be executed at first, followed by the OnGet method of the IndexModel class.
Let's see what the log output has printed for us:
We are immediately seeing from the above image, that the Transient objects are always different. The OperationId value of the ITransientOperation implementation is different inside the IndexModel class, than the one injected inside MyMiddleware class. That proves our point that services registered with the transient lifetime scope are always created again each time they are requested from the DI service container.
But what about the Scoped and Singleton services? We see from the above image that both have the same values in the IndexModel and in the MyMiddleware classes. This simply means that the same instance has been injected inside both of these classes by the service container. Hmmm, let's dig a little bit more on that to arrive at a conclusion about these two types of scope lifetimes.
I will click again on the "Home" menu item and this will trigger another GET request to arrive on the server. Let's observe what we will get in the log output again.
Good, so we see that the Singleton value of "d7eb" is the exact same value we had before in both classes. So, this means that singleton objects are the same for every request and for every class that asks for them. They are created the first time they are requested by the service container and every subsequent request (and class) uses the exact same and only instance.
Finally, what about the scoped service? We see that the value now ("4419") is different than the previous request ("f14d"), but once again it is the same for both classes that asked for the IOperationScoped dependency during the serving of this particular request on the server. So, scoped objects are the same during each individual request, but different across each different request that arrives at the server. Scoped lifetime services are created once per client request (connection) to the server.
You can find the code for the above example here.
That's it for today. Cheers!