How YOU can Learn Dependency Injection in .NET Core and C#
Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris
This is an intro to Dependency Injection, also called DI. I plan a follow-up post covering more advanced scenarios. For now, I want to explain what it is and how you can use it and finally show how it helps to test A LOT.
What is Dependency Injection
It's a programming technique that makes a class independent of its dependencies.
English?
We don't rely on a concrete implementation of our dependencies, but rather interfaces. This makes our code more flexible and we can easily switch out a concrete implementation for another while maintaining the same logic.
References
- Overview of Dependency Injection
- DI MVC example
- DI in Controllers
- Dependency Injection pattern by Martin Fowler
- Overview of Dependency Injection namespace
Why use it
There are many advantages:
- Flexible code, we can switch out one implementation for another without changing the business logic.
- Easy to test, because we rely on interfaces over implementations - we can more easily test our code without worrying about side-effects. We will show this later in the article.
DI in .NET Core - it's built-in
There's a built-in Dependency Injection container that's used by a lot of internal services like:
- Hosting Environment
- Configuration
- Routing
- MVC
- ApplicationLifetime
- Logging
The container is sometimes referred to as IoC, Inversion of Control Container.
The overall idea is to Register at the application startup and then Resolve at runtime when needed.
Container responsibilities:
- Creating
- Disposing
Key concepts:
- IServiceCollection, Register services, lets the IoC container know of concrete implementation. It should be used to resolve what Interface belongs to what implementation. How something is created can be as simple as just instantiating an object but sometimes we need more data than that.
- IServiceProvider, Resolve service instances, actually looking up what interface belongs to what concrete implementation and carry out the creation.
It lives in the Microsoft.Extensions.DependencyInjection
namespace. All the classes and code related to DI can be found there.
What to register
There are some telltale signs.
- Lifespan outside of this method?, Are we new-ing the service, any services our can they live within the scope of the method? I.e are they a dependency or not?
- More than one version, Can there be more than one version of this service?
- Testability, ideally you only want to test a specific method. If you got code that does a lot of other things in your method, you probably want to move that to a dedicated service. This moved code would then become dependencies to the method in question
- Side-effect, This is similar to the point above but it stresses the importance of having a method that does only one thing. If a side-effect is produced, i.e accessing a network resource, doing an HTTP call or interacting with I/O - then it should be placed in a separate service and be injected in as a dependency.
Essentially, you will end up moving out code to dedicated services and then inject these services as dependencies via a constructor. You might start out with code looking like so:
public void Action(double amount, string cardNumber, string address, string city, string name)
{
var paymentService = new PaymentService();
var successfullyCharged = paymentService.Charge(int amount, cardNumber);
if (successfullyCharged)
{
var shippingService = new ShippingService();
shippingService.Ship(address, city, name);
}
}
2
3
4
5
6
7
8
9
10
11
The above has many problems:
- Unwanted side-effects when testing, The first problem is that we control the lifetime of
PaymentService
andShippingService
, thus risking firing off a side-effect, an HTTP call, when trying to test. - Can't test all paths, we can't really test all paths, we can't ask the PaymentService to respond differently so we can test all execution paths
- Hard to extend, will this PaymentService cover all the possible means of payment or would we need to add a lot of conditional code in this method to cover different ways of taking payment if we added say support for PayPal or a new type of card, etc?
- Unvalidated Primitives, there are primitives like
double
andstring
. Can we trust those values, is theaddress
a valid address for example?
From the above, we realize that we need to refactor our code into something more maintainable and more secure. Turning a lot of the code into dependencies and replacing primitives with more complex constructs, is a good way to go.
The result could look something like this:
class Controller
private readonly IPaymentService _paymentService;
private readonly IShippingService _shippingService;
public void Controller(
IPaymentService paymentService,
IShippingService shippingService
)
{
_paymentService = paymentService;
_shippingService = shippingService;
}
public void Action(IPaymentInfo paymentInfo, IShippingAddress shippingAddress)
{
var successfullyCharged = _paymentService.Charge(paymentInfo);
if (successfullyCharged)
{
_shippingService.Ship(ShippingAddress);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Above we have turned both the PaymentService
and ShippingService
into dependencies that we inject in the constructor. We also see that all the primitives have been collected into the complex structures IShippingAddress
and IPaymentInfo
.
Dependency Graph
When you have a dependency it might itself rely on another dependency being resolved first and so on and so forth. This means we get a hierarchy of dependencies that need to be resolved in the right order for things to work out. We call this a Dependency Graph.
DEMO - registering a Service
We will do the following:
- create a .NET Core solution
- add a webapi project to our solution
- fail, see what happens if we forgot to register a service. It's important to recognize the error message so we know where we went wrong and can fix it
- registering a service, we will register our service and we will now see how everything works
Create a solution
mkdir di-demo
cd di-demo
dotnet new sln
2
3
4
this will create the following structure:
-| di-demo
---| di-demo.sln
2
Create a WebApi project
dotnet new webapi -o api
dotnet sln add api/api.csproj
2
The above will create a webapi
project and add it to our solution file.
Now we have the following structure:
-| di-demo
---| di-demo.sln
---| api/
2
3
fail
First, we will compile and run our project so we type:
dotnet run
First time you run the project the web browser might tell you something like your connection is not secure. You have a dev cert that's not trusted. Fortunately, there's a built-in tool that can fix this so you can run a command like this:
dotnet dev-certs https --trust
For more context on the problem:
https://www.hanselman.com/blog/DevelopingLocallyWithASPNETCoreUnderHTTPSSSLAndSelfSignedCerts.aspx
You should have something like this running:
Ok then, we don't have an error but let's introduce one.
Let's do the following:
- Create a controller that supports getting products, this should inject a
ProductsService
- Create a ProductsService, this should be able to retrieve Products from a data source
- **Create a IProductsService interface, inject this interface in the controller
Add a ProductsController
Add the file ProductsController.cs
with the following content:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Services;
namespace api.Controllers
{
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductsService _productsService;
public ProductsController(IProductsService productsService) {
_productsService = productsService;
}
[HttpGet]
public IEnumerable<Product> GetProducts()
{
return _productsService.GetProducts();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Note how we inject the IProductsService
in the constructor. This file should be added to the Controllers
directory.
Add a ProductsService
Let's create a file ProductsService.cs
under a directory Services
, with the following content:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Services {
public class Product {
public string Title { get; set; }
}
public class ProductsService: IProductsService
{
private readonly List<Product> Products = new List<Product>
{
new Product { Title= "DVD player" },
new Product { Title= "TV" },
new Product { Title= "Projector" }
};
public IEnumerable<Product> GetProducts()
{
return Products.AsEnumerable();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Create an interface IProductsController
Let's create the file IProductsController.cs
under the Services
directory, with the following content:
using System;
using System.Collections.Generic;
namespace Services
{
public interface IProductsService
{
IEnumerable<Product> GetProducts();
}
}
2
3
4
5
6
7
8
9
10
Run
Let's run the project with:
dotnet build
dotnet run
2
We should get the following response in the browser:
It's failing, just like we planned. Now what?
Well, we fix it, by registering it with our container.
Registering a service
Ok, let's fix our problem. We do so by opening up the file Startup.cs
in the project root. Let's find the ConfigureServices()
method. It should have the following implementation currently:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
2
3
4
Let's change the code to the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IProductsService, ProductsService>();
services.AddControllers();
}
2
3
4
5
The call to services.AddTransient()
registers IProductsService
and associates it with the implementing class ProductsService
. If we run our code again:
dotnet run
Now your browser should be happy and look like this:
Do we know all we need to know now?
No, there's lots more to know. So please read on in the next section to find out about different lifetimes, transient is but one type of lifetime type.
Service lifetimes
The service life time means how long the service will live, before it's being garbage collected. There are currently three different lifetimes:
- Transient,
services.AddTransient()
, the service is created each time it is requested - Singleton,
services.AddSingleton()
, created once for the lifetime of the application - Scoped,
services.AddScoped()
, created once per request
So when to use each kind?
Good question.
Transient
So for Transient, it makes sense to use when you have a mutable state and it's important that the service consumer gets their own copy of this service. Also when thread-safety is not a requirement. This is a good default choice when you don't know what life time to go with.
Singleton
Singleton means that we have one instance for the lifetime of the application. This is good if we want to share state or creating the Service is considered expensive and we want to create it only once. So this can boost performance as it's only created once and garbage collected once. Because it can be accessed by many consumers thread-safety is a thing that needs to be considered. A good use case here is a memory cache but ensure you are making it thread-safe. Read more here about how to make something thread-safe:
http://www.albahari.com/threading/part2.aspx
Read especially about the lock
keyword in the above link.
Scoped
Scoped means it's created once per request. So all calling consumers within that request will get the same instance. Examples of scoped services are for example DbContext
for Entity Framework. It's the class we use to access a Database. It makes sense to make it scoped. We are likely to do more than one call to it during our request and the resource should be scoped to that specific request/user.
Here be dragons
There's such a thing as captured dependencies. This means that a service lives longer than expected.
So why is that bad?
Well, you want services to live according to their lifetime, otherwise, we take up unnecessary space in memory.
How does it happen?
When you start depending on a Service with a shorter lifetime than yourself you are effectively capturing it, forcing it to stay around according to your lifetime. Example:
You register a ProductsService
with a scoped lifetime and an ILogService
with a transient lifetime. Then you inject the ILogService
into the ProductsService
constructor and thereby capturing it.
class ProductsService
{
ProductsService(ILogService logService)
{
}
}
2
3
4
5
6
7
Don't do that!
If you are going to depend on something ensure that what you inject has an equal or longer life time than yourself. So either change what you depend on or change the lifetime of your dependency.
Summary
We have explained what Dependency Injection is and why it's a good idea to use it. Additionally, we have shown how the built-in container helps us register our dependencies. Lastly, we've discussed how there are different lifetimes for a dependency and which one we should be choosing.
This was the first part of the built-in container. I hope you are excited about a follow-up post talking about some of its more advanced features.