Dependency Injection ASP.NET Core (.NET 8). Example Project GitHub
If you want to learn about Dependency Injection, you’ve come to the right place. Its one of my favorite topics in all of programming. It is however, useful to understand some context and prerequisites.
To understand Dependency Injection in ASP.NET Core you first should have a solid understanding of interfaces. I have another blog article that goes in depth on interfaces to be sure to check that out here.
The application we will build in this tutorial will be quite simple and consist of a single project. If you are interested in seeing how Dependency Injection is used in a wider context, check out the Nano ASP.NET Boilerplate. The Nano ASP.NET Boilerplate is a great learning resource with detailed documentation on concepts like clean architecture, programming patterns and practices. It’s a much simpler starter template than alternatives like the ABP framework.
The main reasons to use Dependency Injection:
- Makes your application easier to maintain
- Makes things modular and improves reusability in your app
- It’s built into .NET Core and is the de facto architecture to follow
Download the sample app code from Github here to follow along. This code is the same used in the interfaces tutorial, but this article focuses are different areas. (.NET 7)
Application Structure (Service Pattern)
Let’s first consider our application structure. By creating this project as a ‘Web API’ project, .NET will create a Controllers folder add these lines in Program.cs. This will look the same in .NET 6 and .NET 7.
var builder = WebApplication.CreateBuilder(args); // <--- 1. Create the Builder (ASP.NET convention)
// SERVICES
// default services added to the service container when creating 'web api project' template
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build(); // <--- 2. Build the App (ASP.NET convention)
// MIDDLEWARE
// default middleware when choosing create 'web api project'
// Optional: add swagger when running in development mode
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run(); // <--- 3. Run the App (ASP.NET convention)
If you had created a ‘Web App’ project (razor pages), there would be a Pages folder with sample razor pages. Razor pages support would be added in place of API controllers, but you can easily add API support and use API controllers with razor pages in the same project.
If you have a very simple application with all of your business logic residing directly in your API controllers, then Dependency Injection won’t make a ton of sense. If you are like me, you learned by following simple tutorials or maybe a beginner course like Mosh Hamedani ASP MVC 5 course, and put all your application code in the controllers.
The service pattern will keep the app modular and reduce repetition. In a nutshell you should:
- Keep your API and view controllers lean.
- Create services (classes) that are responsible for one business entity.
- Consume services in controllers or in other services
- Inject services via class constructors, aka use ‘dependency injection’
The service pattern has been around for a while, and its not so difficult to understand. What’s new is the last point, about using Dependency Injection to handle the orchestration and is the default pattern to follow in .NET.
In the sample application, I’ve created a new folder called Services and this is where I’m keeping all the app services. Within the services folder, I’ve subdivided the services into either Application (business logic specific to my app) or Infrastructure (generic functionality like Mailer, Image Uploader, Etc.). While this is one setup you could follow to organize your application, an even more sophisticated way of structuring things is known as Clean Architecture.
Clean Architecture is also known as Hex or Onion Architecture, and the idea is to move services to other projects (class libraries), with each class library acting as a layer in the architecture, typically Domain, Application, API, Infrastructure. I will write an article on this Clean Architecture later.
The main benefit to allowing the DI framework handle dependency orchestration, is that our app is more maintainable. Without DI, we would need to use the classes directly as opposed having an interface sit in between as an abstraction. In an example situation where we need to change one mailer service (class) for another, we would need to rewrite the code (usually in the class constructors) in every class that uses the mailer class.
Using the DI framework also has other benefits like facilitating mock testing, and taking care of disposing objects when they are not being used anymore.
In this example simpleApp project we are doing data retrieval directly in our service classes. Its common practice to move the data retrieval logic to an even ‘lower-level’ repository class, but for our example, we’ll just keep everything in the service classes.
Creating a service class and interface
Our first requirement in using Dependency Injection is to create an interface for the service. For example, if we have a CRUD (create, read, update, delete) style business service class called ProductService, then we also need a corresponding interface for that class. Here is what our service class looks like:
using simpleApp.Models;
using simpleApp.Services.Application.ProductService.DTOs;
namespace simpleApp.Services.Application.ProductService
{
public class ProductService : IProductService
{
private readonly ApplicationDbContext _context; // database context
public ProductService(ApplicationDbContext context)
{
_context = context;
}
// get a list of all products
public IEnumerable<Product> GetAllProducts()
{
var products = _context.Products.ToList();
return products;
}
// get a single product
public Product GetProductById(int id)
{
var product = _context.Products.Where(x => x.Id == id).FirstOrDefault();
return product;
}
// create a new product
public Product CreateProduct(CreateProductRequest request)
{
var product = new Product();
product.Name = request.Name;
product.Price = request.Price;
_context.Add(product);
_context.SaveChanges();
return product;
}
// delete a product
public bool DeleteProduct(int id)
{
var product = _context.Products.Where(x => x.Id == id).FirstOrDefault();
if (product != null)
{
_context.Remove(product);
_context.SaveChanges();
return true;
}
return false;
}
}
}
The ProductService class has methods for getting a list of products, a single product by ID, creating a product, and deleting a product. At the start of the class, where we declare the name of the class, you can see that the IProductService interface is being implemented. This is what our interface looks like:
using simpleApp.Models;
using simpleApp.Services.Application.ProductService.DTOs;
namespace simpleApp.Services.Application.ProductService
{
public interface IProductService
{
IEnumerable GetAllProducts();
Product GetProductById(int id);
Product CreateProduct(CreateProductRequest request);
bool DeleteProduct(int id);
}
}
Now that we have the service class and corresponding interface, we are ready to register this service in the top level of our application, the program.cs class. Once we do that, the ProductService class will be injected anywhere IProductService is found.
The Program.CS Class
The Program.cs class is the top-level class in our application. In .NET core, things related to app startup and configuration have become very simple. The entire bootstrapping process of a .NET Core application goes in the program.cs class.
The process is like this:
- Create a builder object from WebApplication.CreateBuilder()
- Add services to the Dependency Injection Service Collection
- Use the Build() method to create the app
- Add middleware
- Run the app with the Run() method
Every .NET Core web application will start with creating a builder. This builder has a collection called Services and it is with this Service Collection container that we will register all of our application services, including ProductService.
If you specify a ‘web api project’ when you create a new project, you might notice that .NET has already added a few services, like AddControllers(), AddEndpointsApiExplorer(), AddSwaggerGen(). In the simpleApp example, there’s another service AddDbContext() which is adding in database access (using Entity Framework).
As you see, even critical base functionality is added via this method of service registration, it’s not just our custom application and infrastructure services.
To register your own classes, you will use either AddTransient(), AddScoped(), or AddSingleton(). These methods are always passed two parameters, the interface (first parameter) and the class to implement (second parameter). This is the all-important dependency injection link, with specified lifetime (explained in next section). With this in place, DI will inject the specified class wherever it finds the interface.
The order in which you register the services is not that important. Generally speaking, you should register your services after the database registration and before the builder.Build() method is called.
Base level services often handle registration within extension methods. Extension methods are abstractions and can be used to move registration/configuration code out of the program.cs class. Take for example, AddControllers() or AddEndpointsApiExplorer() — these are extension methods, and if you dig into them you would find the same AddTransient, AddScoped, or AddSingleton calls among other related code. Developers commonly create an extension method called AddServices() and group all the code relating to adding custom services in this extension to keep the program.cs class nice and organized.
Registration Lifetimes
When you register services to the DI services container, you must specify one of three lifetime options, and then pass in the interface as the first parameter and the actual class as the second parameter.
When we register our services, we have three options:
- Singleton
- Scoped
- Transient
Singleton means that an instance of the service will run from the moment the application starts until shut down. Something that might use singleton is perhaps a logger, a task scheduler, or something that needs to be running in the background all the time. This one instance could be running for days or months at a time. It’s the least common registration type of the three.
Scoped means that the app will create a new instance of the server for each API request. In other words, that’s pretty short, probably measured in the milliseconds. However, if in a single request, the service is invoked more than once, the app will use the same instance.
Transient is similar to scoped but its lifetime is even shorter. With transient, the app will create a new instance of the class anytime its invoked. This is actually the most common registration type and the one you should use with CRUD style services. Don’t forget that your application could be handling hundreds or thousands of requests every minute, so being able to specify how long resources are allocated to services provides great control on making efficient apps.
Registering the Product Service
To register the Product service to the app’s Service Collection container, all we need to do is add the following line in the program.cs class. We’ll add this line after our database service is registered but before the builder.build() method.
// adding a CRUD product service
builder.Services.AddTransient<IProductService, ProductService>();
With this, our product service is now registered as a transient service which we can use throughout our application.
In .NET Core applications, you need to register every service you create. This can be an easy step to forget and it may seem a bit repetitive if your app contains many services. Often what developers to is create extension methods to organize their code, for example moving all of the service registrations to a new class. The Nano ASP.NET Boilerplate does this and goes a step further by introducing automation to register services based on interface types like ITransientService and IScopedService.
Next we will use the product service in our product controller which contains the API endpoints.
Using the Product Service
Now that our service is registered, step three is to inject the service wherever we want to use it. We do that within a class’s constructor. In the case of ProductsController, the code will look like this.
using Microsoft.AspNetCore.Mvc;
using simpleApp.Services.Application.ProductService;
using simpleApp.Services.Application.ProductService.DTOs;
// example CRUD style endpoint with service
namespace simpleApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService; // injecting the product service
// using interfaces in Dependency Injection
public ProductsController(IProductService productService)
{
_productService = productService;
}
// Get list of products
[HttpGet]
public IActionResult Get()
{
var list = _productService.GetAllProducts(); // using the service
return Ok(list);
}
// Create a new product
[HttpPost]
public IActionResult Post(CreateProductRequest request)
{
try
{
var result = _productService.CreateProduct(request); // also using the service
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
// Update product
// Delete product
}
}
By using the IProductsService interface in the constructor’s parameters instead of the actual ProductService class, we are telling .NET to use Dependency Injection. In other words, whatever class we have specified at the top level in our service container, .NET will use it here. That makes things much easier to manage going forward. If later, we have a ProductServiceV2 class that we want to use instead of ProductService, we will just change the service registration, a single line of code in our program.cs,
In the service registration, the interface will remain the same (first parameter), but the class will change to the new implementation (second parameter), thus directing .NET to use this new class wherever the old one was being pulled down.
Injecting the service is done with the constructor method, 99% of the time. It’s simply, using the interface as the type instead of the class with the class constructor as we have just done.
In situations where the app needs access to a service during start up, there is one other way to use services from DI.
using var scope = app.ApplicationServices.CreateScope();
var services = scope.ServiceProvider;
var productService = services.GetRequiredService<IProductService>();
This code is not found in the simpleApp example but is shown here for reference. We create a scope from the app object using ApplicationServices.CreateScope(). Then we create a services variable from the scope ServiceProvider(). Finally we use the GetRequiredService<T>() method on the scope, passing in either the class or the interface of the service we need. A database seeder would be a good example of a real world scenario where this is needed.
Wrapping Up
So now you can begin to see all the ways that Dependency Injection used with interfaces and the services pattern helps us.
There would of course be a lot of code that gets added to the program.cs file, and it can become busy rather quickly. As mentioned earlier, for that you can create extension methods and move code (like service registrations) into another class for better organization.
ASP.NET Core Differences from ASP.NET MVC 5
If you are coming from ASP.NET MVC 5 (like I was) you may be wondering, how did we do this before?
Before .NET Core, dependency injection was not part of the framework. Instead, you needed to use a 3rd party nuget package like Autofac, Ninject, or Castle Windsor. They each have their own code patterns, quirks and features.
The service pattern was common though, the main difference being that you would use the service classes directly. As follows, there was no service registration code in the top-level class. In fact, the top-level class situation was a whole other story in MVC 5 and quite confusing by comparison. As a result, MVC5 apps were tightly coupled applications unless Dependency Injection was handled with an extra library. Today .NET is much more streamlined and now is a good time to start learning. Of course, an amazing resource for building applications with .NET is the Nano ASP.NET Boilerplate. Thanks for reading, I hope you learned something!
Hi Ryan
Thanks for sharing you knowledge!