DEV Community

Cover image for Dependency Injection Basics in C#
AllCoderThings
AllCoderThings

Posted on

Dependency Injection Basics in C#

Originally published at https://allcoderthings.com/en/article/csharp-dependency-injection-basics

In software development, Dependency Injection (DI) is an important design pattern used to manage dependencies. With DI, classes receive the objects they need from the outside instead of creating them internally. This makes code more flexible, testable, and maintainable. In the C# and .NET Core ecosystem, DI is supported out of the box.


What is a Dependency?

A class depends on another class when it requires its functionality. For example, an OrderService may need an ILogger instance when creating an order. If OrderService directly uses new Logger(), it becomes tightly coupled to the Logger class.

public class OrderService
{
    private readonly Logger _logger = new Logger();

    public void CreateOrder(string product)
    {
        _logger.Log($"Order created: {product}");
    }
}

public class Logger
{
    public void Log(string message) => Console.WriteLine(message);
}
Enter fullscreen mode Exit fullscreen mode

In this approach, the Logger cannot be replaced. If you want to use a different logging system, you must modify the OrderService code. This is where Dependency Injection comes into play.


Solution with Dependency Injection

When DI is used, dependencies are abstracted and provided from the outside (via constructor or method parameter). This way, OrderService depends on the ILogger interface, and the external world decides which logger to use.

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine("[Console] " + message);
}

public class FileLogger : ILogger
{
    public void Log(string message) => 
        System.IO.File.AppendAllText("log.txt", message + "\n");
}

public class OrderService
{
    private readonly ILogger _logger;

    // Constructor Injection
    public OrderService(ILogger logger)
    {
        _logger = logger;
    }

    public void CreateOrder(string product)
    {
        _logger.Log($"Order created: {product}");
    }
}

class Program
{
    static void Main()
    {
        // Different loggers can be selected
        ILogger logger = new ConsoleLogger();
        var service = new OrderService(logger);

        service.CreateOrder("Laptop");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, OrderService is no longer tied to a specific Logger class; it only depends on the ILogger interface. Therefore, ConsoleLogger or FileLogger can be easily swapped.


Types of Dependency Injection

  • Constructor Injection: Dependencies are provided through the constructor (most common method).
  • Property Injection: Dependencies are assigned through public properties.
  • Method Injection: Dependencies are passed via method parameters.

Using DI in .NET Core

In .NET Core applications, a built-in DI container is available. Services are registered in Program.cs or Startup.cs, and they are automatically injected into classes where needed.

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddScoped<ILogger, ConsoleLogger>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

app.MapGet("/order", (OrderService service) =>
{
    service.CreateOrder("Phone");
    return "Order processed.";
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

Here, the DI container automatically creates and injects a ConsoleLogger instance for OrderService.


Service Lifetimes (Service Lifecycles)

When registering services in the .NET Core DI container, you must choose a lifetime. The lifetime determines how long a service instance lives and how often a new instance is created.

  • Transient: A new instance is created every time it is requested.
  • Scoped: A single instance is created per HTTP request. The same instance is reused within that request.
  • Singleton: A single instance is created when the application starts and reused for the entire lifetime of the application.
// Transient
builder.Services.AddTransient<ILogger, ConsoleLogger>();

// Scoped
builder.Services.AddScoped<ILogger, ConsoleLogger>();

// Singleton
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
Enter fullscreen mode Exit fullscreen mode
  • Use Transient for lightweight, stateless services.
  • Use Scoped for request-based services (e.g., DbContext).
  • Use Singleton for shared, application-wide services.

Advantages

  • Loose Coupling: Classes depend on interfaces rather than specific implementations.
  • Testability: Unit testing is easier with mock or fake objects.
  • Flexibility: Different implementations can be swapped easily.
  • Maintainability: Dependencies are managed centrally, improving readability and maintainability.

TL;DR

  • Dependency Injection: Classes receive the objects they need from outside.
  • Constructor Injection: The most common approach; dependencies are passed via constructors.
  • .NET Core: Provides a built-in DI container.
  • Benefits: Enables more flexible, testable, and maintainable architecture.

Top comments (0)