A dependency is an object that another object depends on. Dependency injection (DI) is a software design pattern that allows us to separate the dependencies of a class from its implementation. This makes our code more loosely coupled and easier to test.
A code dependency occurs when one class directly depends on another for its functionality. In such a scenario, the dependent class creates an instance of the required class, leading to tight coupling.
when class A uses some functionality of class B, then it’s said that class A has a dependency of class B. For example, in C#, before we can use methods of other classes, we first need to create the object of that class (i.e. class A needs to create an instance of class B).
In this example, the OrderProcessor class depends directly on the EmailService class.
public class EmailService
{
public void SendEmail(string to, string subject, string body)
{
Console.WriteLine($"Sending email to {to} with subject '{subject}'");
}
}
public class OrderProcessor
{
private readonly EmailService _emailService;
public OrderProcessor()
{
// Direct dependency
_emailService = new EmailService();
}
public void ProcessOrder(string customerEmail)
{
Console.WriteLine("Processing order...");
// Using the dependency
_emailService.SendEmail(customerEmail, "Order Confirmation", "Your order has been placed.");
}
}
// Example Usage
class Program
{
static void Main()
{
var orderProcessor = new OrderProcessor();
orderProcessor.ProcessOrder("customer@example.com");
}
}
Although the OrderProcessor depends on the IEmailService, the EmailService class is tightly coupled to Console.WriteLine.
Tight Coupling
Lack of Dependency Injection
Reduced Testability
No Abstraction
No Error Handling
Injecting Dependency means that you push the dependency (object of class B) into class A from outside.
We can explain it as a technique where an object supplies the dependencies to another object. This pattern allows someone to remove hard-code dependencies and make it possible to change them. Dependencies can be injected to the object via the constructor; via the defined methods or a setter property.
It is not possible to inject this type of code in ASP.NET Core.
ASP.NET Core provides you with extensive support to Dependency Injection. ASP.NET Core injects objects of dependency classes through constructor or method by using built-in Inversion of Control (IoC) container.
DI aids loose coupling, and loose coupling makes code more maintainable. The foremost idea of using DI is to decouple your classes constructor from the construction of its dependencies, so that you will not have to instantiate anything in the dependent class.
So if we redesign the application according to the IoC principle, then our design can be depicted by the following diagram.
So Class A now depends upon an abstraction such as an interface or an abstract class.
For example
let’s say we have a Car class that contains many objects like wheels, engine, etc. The car class is responsible for creating all the dependency objects. Suppose, if we decide to discontinue MRF Wheels in the future and want to use Bridgestone Wheels? We will need to recreate the car object with a new dependency. But when using dependency injection (DI), we can change the Wheels at runtime as dependencies can be injected at runtime rather than at compile time.
You can think of DI as the middleman in our code who does all the work of creating the preferred wheels object and providing it to the Car class. It makes our Car class independent from creating the objects of Wheels, Battery, etc.
Create an interface that specifies the behavior of the service.
public interface IGreetingService
{
string Greet(string name);
}
Create a class that implements the interface.
public class GreetingService : IGreetingService
{
public string Greet(string name)
{
return $"Hello, {name}!";
}
}
Register the service in the Startup.cs
(or
Program.cs
in .NET 6 and above) file.
var builder = WebApplication.CreateBuilder(args);
// Register the service in the dependency injection container
builder.Services.AddScoped<IGreetingService, GreetingService>();
var app = builder.Build();
Use constructor injection to inject the service into a controller.
[ApiController]
[Route("api/[controller]")]
public class GreetingController : ControllerBase
{
private readonly IGreetingService _greetingService;
public GreetingController(IGreetingService greetingService)
{
_greetingService = greetingService;
}
[HttpGet("{name}")]
public IActionResult GetGreeting(string name)
{
var message = _greetingService.Greet(name);
return Ok(message);
}
}
Lifecycle Options in Dependency Injection
We use different service lifetimes to manage how services are created and disposed of within the application's lifecycle. This helps us control the memory usage and performance of our application more efficiently. Here’s a breakdown of why each type is useful.
Here’s a breakdown of why each type is useful:
Purpose - Services with a transient lifetime are created each time they're requested.
Use Case - When you need a new instance for each operation. For example, lightweight services that don't maintain any state between calls.
Real World Example -
Sending Email Notifications to Users - Each email sent is a
separate operation, and there is no shared state between email sends. Using
Transient ensures that a new instance is created for each email, preventing
any unintended side effects.
Examplepublic interface IEmailNotificationService
{
void SendEmail(string to, string subject, string body);
}
public class EmailNotificationService : IEmailNotificationService
{
public void SendEmail(string to, string subject, string body)
{
// Logic to send email
}
}
// Registration in Startup.cs
services.AddTransient<IEmailNotificationService, EmailNotificationService>();
Purpose - Scoped services are created once per request or per HTTP request. Within a single request, all parts of the application using the IShoppingCartService get the same instance.
Use Case - When you need a service that maintains state for the duration of a request. For example, handling user data specific to a single web request in a web application.
public interface IShoppingCartService
{
void AddItem(string productId, int quantity);
void RemoveItem(string productId);
decimal CalculateTotal();
}
public class ShoppingCartService : IShoppingCartService
{
private readonly List<CartItem> _items = new();
public void AddItem(string productId, int quantity)
{
_items.Add(new CartItem { ProductId = productId, Quantity = quantity });
}
public void RemoveItem(string productId)
{
_items.RemoveAll(item => item.ProductId == productId);
}
public decimal CalculateTotal()
{
// For simplicity, assume each item costs $10
return _items.Sum(item => item.Quantity * 10);
}
}
public class CartItem
{
public string ProductId { get; set; }
public int Quantity { get; set; }
}
builder.Services.AddScoped<IShoppingCartService, ShoppingCartService>();
[ApiController]
[Route("api/[controller]")]
public class ShoppingCartController : ControllerBase
{
private readonly IShoppingCartService _shoppingCartService;
public ShoppingCartController(IShoppingCartService shoppingCartService)
{
_shoppingCartService = shoppingCartService;
}
[HttpPost("add")]
public IActionResult AddItem(string productId, int quantity)
{
_shoppingCartService.AddItem(productId, quantity);
return Ok("Item added.");
}
[HttpPost("remove")]
public IActionResult RemoveItem(string productId)
{
_shoppingCartService.RemoveItem(productId);
return Ok("Item removed.");
}
[HttpGet("total")]
public IActionResult CalculateTotal()
{
var total = _shoppingCartService.CalculateTotal();
return Ok($"Total: {total}");
}
}
Purpose - Singleton services are created the first time they're requested and then shared throughout the application's lifetime.
Use Case - When you need to share data or functionality across the entire application. For example, caching data or a logging service.
Real World Example - Configuration Service
Suppose you have a configuration service that reads
settings from a configuration file (e.g., appsettings.json
).
These settings are read once when the application starts and remain
unchanged during the application's lifetime. Singleton scope is ideal in
this case because:
A logging service is often implemented as a singleton. Logging is a cross-cutting concern that needs to be available across the application and should ideally write to a shared resource, such as a file, database, or logging service (e.g., Seq, Application Insights, or Serilog).
Using a Singleton ensures:
public class LoggingService
{
private readonly ILogger<LoggingService> _logger;
public LoggingService(ILogger<LoggingService> logger)
{
_logger = logger;
}
public void LogInfo(string message)
{
_logger.LogInformation(message);
}
public void LogError(string message, Exception ex)
{
_logger.LogError(ex, message);
}
}
In the Program.cs or Startup.cs
builder.Services.AddSingleton<LoggingService>();
3. Consuming the Logging Service
public class OrderService
{
private readonly LoggingService _loggingService;
public OrderService(LoggingService loggingService)
{
_loggingService = loggingService;
}
public void ProcessOrder(int orderId)
{
try
{
// Order processing logic
_loggingService.LogInfo($"Order {orderId} processed successfully.");
}
catch (Exception ex)
{
_loggingService.LogError($"Error processing order {orderId}.", ex);
}
}
}