Dependency Injection in .NET Core
Maurizio8788
Posted on October 18, 2023
Dependency Injection (from now on DI) is a design pattern that allows you to write modular and testable code. In .NET Core, we are fortunate to have a native library that helps us implement this pattern.
In this article, we will cover the basics needed to implement it in .NET Core 6 using the basic extension methods associated with the IServiceCollection interface, namely, AddSingleton, AddScoped, and AddTransient.
Table of Contents:
AddSingleton
Everyone working in our industry knows, or at least has heard of, the Singleton design pattern, which is the anti-pattern that ensures a class has one and only one instance throughout the application lifecycle, and that's precisely what the AddSingleton method does.
The classic implementation of a Singleton is as follows:
public class MySingleton : IMySingleton {
public static MySingleton instance;
private MySingleton(){}
public void CreateInstance(){
if(instance == null) {
instance = new MySingleton();
}
return instance;
}
// other methods...
}
Now, when we implement it using .NET Core's DI, we simply use these instructions, and we can then inject it anywhere as needed:
// MySingleton.cs
public class MySingleton : IMySingleton {
// other methods...
}
// Program.cs
builder.Service.AddSingleton<IMySingleton, MySingleton>();
// MyEndpointController.cs
[ApiController]
[Route("api/[controller]")]
public class MyEndpointController : ControllerBase {
private readonly IMySingleton _mySingleton;
public MyEndpointController(IMySingleton mySingleton){
_mySingleton = mySingleton;
}
// other endpoints...
}
The entire process of creating an instance of the class at the application's startup is delegated to the .NET Core DI engine. Please note that Singleton classes are created and exist until the application is shut down or restarted, so use them sparingly.
AddTransient
These are services that are created on-demand by the service container. They are typically used for stateless services, and their usage is quite similar to Singleton.
// MyTransientService.cs
public class MyTransientService : IMyTransientService {
public string MethodOfTransientService() {
return "Hello from Transient Service!";
}
}
// Program.cs
builder.Service.AddTransient<IMyTransientService, MyTransientService>();
// MyApplicationService.cs
public class MyApplicationService : IMyApplicationService {
private readonly IMyTransientService _myTransient;
public MyApplicationService(IMyTransientService myTransient) {
_myTransient = myTransient;
}
public void MyAppServiceMethod() {
var result = _myTransient.MethodOfTransientService();
Console.WriteLine(result);
}
}
AddScoped
It creates services once per client request, regardless of whether they are actually used or not. These services are typically used for registering services that need to be readily accessible, such as the AddDbContext
in Entity Framework Core, which is a scoped service.
// Product.cs
public class Product {
public int Id { get; set; }
public string Name { get; set; }
}
// ApplicationDbContext.cs
public class ApplicationDbContext : DbContext {
public DbSet<Product> Products { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) {
}
}
// ProductService.cs
public class ProductService : IProductService {
private readonly ApplicationDbContext dbContext;
public ProductService(ApplicationDbContext context) {
dbContext = context;
}
public void AddProduct(string productName) {
var product = new Product { Name = productName };
dbContext.Products.Add(product);
dbContext.SaveChanges();
}
public List<Product> GetProducts() {
return dbContext.Products.ToList();
}
}
// Program.cs
builder.Service.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite("Data Source=products.db"));
builder.Service.AddScoped<IProductService, ProductService>();
// OrderService.cs
public class OrderService : IOrderService {
private readonly IProductService productService;
public OrderService(IProductService productService) {
this.productService = productService;
}
public void PlaceOrder(string productName) {
productService.AddProduct(productName);
Console.WriteLine($"Placed an order for {productName}");
}
}
// MyApplicationService.cs
public class MyApplicationService : IMyApplicationService {
private readonly IOrderService orderService;
public MyApplicationService(IOrderService orderService) {
this.orderService = orderService;
}
public void MyAppServiceMethod() {
orderService.PlaceOrder("Widget");
var products = orderService.GetProducts();
Console.WriteLine($"Products in the order: {string.Join(", ", products)}");
}
}
As you can see, the only difference in this case is the name of the operator used in the Program.cs file, but its usage is identical.
One last thing to keep in mind for this operator is not to inject a service registered with Scoped into a class registered with Singleton.
But why is that? If we think about it, it's logical. The Singleton service is created only once, while the Scoped service is disposed of at the end of the request. It's natural that if you inject a Scoped service into a Singleton, the Scoped service might remain active longer than it should and could be accessed by multiple threads, potentially creating thread-safety issues. This is a concern only when injecting Scoped services into classes registered as Transient and with other Scoped services.
I hope this brief introduction to Dependency Injection in .NET is helpful to all of you, and I look forward to receiving feedback on the article so that we can have a discussion.
Happy Coding!
Posted on October 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.