Live · 1419 articles
ef core

.NET Dependency Injection Patterns

.NET Dependency Injection Patterns - in-depth ef core guide with examples.

ef core7 Apr 2023

By Sandeep Mhaske | January 2025

Introduction

Dependency Injection (DI) is a design pattern that plays a pivotal role in achieving loosely coupled and maintainable applications. With .NET Core, Microsoft introduced a built-in DI container, enabling developers to leverage DI seamlessly in their applications.

This blog delves into the core concepts of DI, explores different injection patterns, and provides practical examples and tutorials to help you master Dependency Injection in .NET.

Understanding Dependency Injection (DI)

Dependency Injection is a technique for providing dependencies to a class rather than allowing the class to create them. By delegating the responsibility of dependency creation, DI promotes:

  • Loose Coupling: Classes are not tightly bound to their dependencies.
  • Improved Testability: Dependencies can be easily mocked for unit testing.
  • Better Maintainability: Changes in dependencies do not require changes in dependent classes.

Dependency Injection Patterns Overview

DI patterns dictate how dependencies are provided to a class. The most commonly used patterns are:

  • Constructor Injection: Dependencies are passed via the constructor.
  • Property Injection: Dependencies are assigned to properties.
  • Method Injection: Dependencies are passed as method parameters.

Constructor Injection

Constructor Injection is the most widely used DI pattern. Dependencies are provided through the constructor when the class is instantiated.

public class ProductService
{
    private readonly IRepository _repository;

    public ProductService(IRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<Product> GetAllProducts()
    {
        return _repository.GetProducts();
    }
}
            

In the above example, the IRepository dependency is injected into the ProductService class via its constructor.

Property Injection

Property Injection involves setting dependencies via public properties. This pattern is useful when the dependency is optional.

public class ProductService
{
    public IRepository Repository { get; set; }

    public IEnumerable<Product> GetAllProducts()
    {
        return Repository.GetProducts();
    }
}
            

Method Injection

Method Injection provides dependencies as parameters to specific methods. This approach is ideal when the dependency is only needed for specific actions.

public class ProductService
{
    public IEnumerable<Product> GetAllProducts(IRepository repository)
    {
        return repository.GetProducts();
    }
}
            

Service Lifetimes in DI

.NET Core DI supports three service lifetimes:

  • Transient: A new instance is created each time the service is requested.
  • Scoped: A single instance is created per request.
  • Singleton: A single instance is created and shared across the application lifecycle.

Real-World Example with Code

Below is an example of configuring DI in a .NET Core application:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddTransient<IRepository, Repository>();
services.AddScoped<IService, Service>();
services.AddSingleton<ILogger, Logger>();
var provider = services.BuildServiceProvider();

var service = provider.GetService<IService>();
            

Best Practices for Dependency Injection

  • Use Constructor Injection as the default pattern.
  • Avoid injecting too many dependencies into a single class.
  • Leverage the built-in DI container in .NET Core for simplicity and consistency.
  • Use scoped services for web applications to handle per-request dependencies.

Conclusion

Dependency Injection is a fundamental concept for designing flexible, maintainable, and testable .NET applications. By understanding the various DI patterns and their appropriate use cases, you can improve the overall architecture of your projects.

Sandip Mhaske
Senior Engineer • Author of 1419 guides • Follow for System Design & .NET deep-dives
About

Service Lifetimes

Transient: new instance per resolution. Scoped: one instance per scope (one per HTTP request). Singleton: one instance for the app lifetime. Choosing wrong causes memory leaks or race conditions.

Common Anti-Patterns

Captive dependencies (transient service holding a scoped one), service locators hiding dependencies, and deep constructor graphs violating the Single Responsibility Principle.

Factory Patterns

Use Func, IServiceProvider, or factory delegates when the implementation isn't known until runtime, or when constructing expensive objects lazily.

Validation at Startup

Call BuildServiceProvider(validateScopes: true) in development to catch lifetime mismatches immediately. Cheap insurance against production bugs.

Sandip Mhaske
Sandip Mhaske
Senior Engineer · Author of 1419 guides · Follow for System Design & .NET deep-dives
About