Dependency Injection in .NetCore


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.

Code Dependency in C#

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.

Dependency Injection in .NetCore

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).

Without Dependency Injection (Tight Coupling)

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.

Issues in the Code

  1. Tight Coupling

    • The OrderProcessor class directly instantiates the EmailService class (_emailService = new EmailService();), creating a tight coupling between the two classes.
    • This makes it hard to substitute EmailService with another implementation (e.g., a mock for testing or a real email service in production).
  2. Lack of Dependency Injection

    • Since the EmailService is instantiated within OrderProcessor, you cannot inject dependencies from the outside. This violates the Dependency Inversion Principle (DIP), a core principle of SOLID design.
  3. Reduced Testability

    • Because OrderProcessor creates its own EmailService, it is difficult to mock or replace EmailService for unit tests.
  4. No Abstraction

    • The EmailService class is used directly, and there is no interface or abstraction (IEmailService) to decouple the implementation from the OrderProcessor.
  5. No Error Handling

    • The SendEmail method does not handle exceptions or validate input parameters, making the system prone to runtime errors.

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.

Dependency Injection in .NetCore

Dependency injection can be divided into three types

  1. Constructor injection - The dependencies are offered via a class constructor.
  2. Method injection - It is used when the instance of a dependency is only required within a single method in the entire component.
  3. Property - It is used when a property of the component holds the dependency instance, and it is resolved during runtime by the container.

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.

Why Use Dependency Injection?

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.

Dependency Injection in .NetCore

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.

Dependency Injection in .NET Core Web API

1. Define a Service Interface

Create an interface that specifies the behavior of the service.

public interface IGreetingService
{
    string Greet(string name);
}

2. Implement the Service

Create a class that implements the interface.

public class GreetingService : IGreetingService
{
    public string Greet(string name)
    {
        return $"Hello, {name}!";
    }
}

3. Register the Service

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();

4. Inject the Service in Controllers

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:

1. Transient

  • 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.

    Example

    public 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>();

2. Scoped

  • 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.

    Real World Example -Shopping Cart Service in an E-Commerce API

    Imagine you are building an e-commerce application where users can add items to a shopping cart during their session. Each HTTP request might involve operations like adding an item, removing an item, or calculating the total price. The shopping cart service should maintain consistency within the scope of the request.

    Code Implementation

    1. Service Definition
      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; }
      }
      
    2. Registering the Service in Program.cs or Startup.cs

      builder.Services.AddScoped<IShoppingCartService, ShoppingCartService>();
    3. Using the Service in a Controller

      [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}");
          }
      }

Why AddScoped Not AddTransient or AddSingleton?

1. AddScoped

  • Advantage - Ensures a single instance per HTTP request. Within a single request, all parts of the application using the IShoppingCartService get the same instance.
  • Relevance - Shopping cart operations within a request (like adding, removing items, and calculating the total) should work on the same in-memory list.

2. AddTransient

  • Problem - A new instance would be created every time the service is injected. This would lead to inconsistency, as the cart data would be lost between calls within the same request.

3. AddSingleton

  • Problem - A single instance is shared across all requests and users. This would cause data to bleed between different users' shopping carts, resulting in a severe security issue.

3. Singleton

  • 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:

    1. The service is lightweight.
    2. It doesn't depend on per-request or transient state.
    3. Re-reading the configuration file for every request is unnecessary and inefficient.

  • Real-World Example - Logging Service

    Scenario:

    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:

    1. A single instance manages all logging throughout the application's lifetime.
    2. Logging operations are thread-safe.
    3. Resource usage (like file handles or network connections) is optimized.

    Implementation

    1. Logging Service

    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);
        }
    }

    2. Registering as a Singleton

    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);
            }
        }
    }

Prev Next

Top Articles

  1. What is JSON
  2. How to convert a javaScript object in JSON object
  3. Some Important JSON Examples
  4. Common JSON Interview Question