Dependency Injection in C# .NET With Examples

Introduction

Dependency injection (DI) is a fundamental concept in modern software development, particularly in the realm of C# .NET. It's a technique that allows you to achieve loosely coupled and maintainable code by decoupling the components of your application. In this article, we'll delve into the world of dependency injection in C# .NET and master this essential skill with practical examples.

What is Dependency Injection?

Dependency injection is a design pattern that focuses on reducing the coupling between different components or classes in your software. In essence, it allows you to provide the dependencies that a class requires from external sources rather than creating them within the class itself. This promotes flexibility, testability, and maintainability in your codebase.

In C# .NET, dependency injection is typically implemented through the use of an inversion of control (IoC) container, which manages the creation and resolution of dependencies.

Setting Up Your Environment

Before diving into examples, ensure you have the following prerequisites:

  1. Visual Studio or Visual Studio Code.
  2. .NET SDK installed (at least version 5.0).

Example 1. Constructor Injection

One of the most common forms of dependency injection is constructor injection. Let's create a simple example to illustrate this concept. Suppose we have a UserService class that depends on a UserRepository.

public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public string GetUserFullName(int userId)
    {
        var user = _userRepository.GetUserById(userId);
        return $"{user.FirstName} {user.LastName}";
    }
}

In this example, UserService accepts an IUserRepository interface through its constructor. The IUserRepository is an abstraction that represents the data access layer.

Now, let's implement a concrete class for the IUserRepository.

public class UserRepository : IUserRepository
{
    public User GetUserById(int userId)
    {
        // Retrieve user from the database
        return /* ... */;
    }
}

With these two classes in place, we can use an IoC container (e.g., Microsoft.Extensions.DependencyInjection) to wire everything up.

var serviceProvider = new ServiceCollection()
    .AddTransient<IUserRepository, UserRepository>()
    .AddTransient<UserService>()
    .BuildServiceProvider();

var userService = serviceProvider.GetService<UserService>();

Here, we register our dependencies and request an instance of UserService from the service provider. The service provider resolves the IUserRepository dependency and injects it into the UserService constructor.

Example 2. Property Injection

Another form of dependency injection is property injection. While constructor injection is generally preferred for mandatory dependencies, property injection can be useful for optional or mutable dependencies.

Let's extend our previous example to include property injection for a logging service.

public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public ILogger Logger { get; set; }

    public string GetUserFullName(int userId)
    {
        Logger?.LogInformation("Getting user full name.");
        var user = _userRepository.GetUserById(userId);
        return $"{user.FirstName} {user.LastName}";
    }
}

In this case, we've added a public Logger property to the UserService. We can set this property using property injection:

var userService = serviceProvider.GetService<UserService>();
userService.Logger = new ConsoleLogger(); // Example of a logger implementation

Example 3. Method Injection

Method injection is less common than constructor or property injection, but it can be useful in certain scenarios. With method injection, you inject dependencies directly into specific methods.

Let's enhance our UserService to accept a logger as a method parameter:

public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public string GetUserFullName(int userId, ILogger logger)
    {
        logger.LogInformation("Getting user full name.");
        var user = _userRepository.GetUserById(userId);
        return $"{user.FirstName} {user.LastName}";
    }
}

You can then call this method and inject the logger as needed:

var userService = serviceProvider.GetService<UserService>();
var logger = new ConsoleLogger(); // Example of a logger implementation
var fullName = userService.GetUserFullName(1, logger);

Conclusion

Dependency injection is a powerful technique for achieving maintainable, testable, and loosely coupled code in C# .NET. In this article, we've explored the three common forms of dependency injection: constructor injection, property injection, and method injection. By mastering these concepts and leveraging an IoC container, you can write more flexible and maintainable software in the C# .NET ecosystem.