How to use inversion of control in C#

By FoxLearn 1/3/2025 6:49:11 AM   67
IoC and dependency injection (DI) both aim to reduce dependencies between components, enhancing testability and maintainability.

What is inversion of control?

Inversion of Control (IoC) is a design pattern that reverses the program's control flow, helping decouple application components. It allows for easy swapping of dependency implementations, mocking dependencies, and making the application more modular and testable.

Dependency Injection is a specific implementation of the Inversion of Control (IoC) principle. While IoC can be implemented using various techniques such as events, delegates, template patterns, factory methods, or service locators, dependency injection is one of its key methods.

The Inversion of Control (IoC) design pattern suggests that objects should not create their own dependencies. Instead, they should receive them from an external service or container. This follows the "Hollywood principle," where the framework calls the application's provided implementation, rather than the application calling the framework's methods.

Inversion of control example in C#

Let's consider you're building an online payment processing system and you need to implement logging. For simplicity, let's say the log target is a text file. Create two files, PaymentService.cs and FileLogger.cs.

public class PaymentService
{
    private readonly FileLogger _fileLogger = new FileLogger();
    public void Log(string message)
    {
        _fileLogger.Log(message);
    }
}

public class FileLogger
{
    public void Log(string message)
    {
        Console.WriteLine("Inside Log method of FileLogger.");
        LogToFile(message);
    }

    private void LogToFile(string message)
    {
        Console.WriteLine("Method: LogToFile, Text: {0}", message);
    }
}

While the above implementation is functional, it has a limitation: logging is constrained to only text files. If you want to log data to a database or other sources, you would need to modify the existing code.

If you wanted to log data to a database, you'd either need to change the FileLogger class or create a new class, such as DatabaseLogger.

public class DatabaseLogger
{
    public void Log(string message)
    {
        Console.WriteLine("Inside Log method of DatabaseLogger.");
        LogToDatabase(message);
    }

    private void LogToDatabase(string message)
    {
        Console.WriteLine("Method: LogToDatabase, Text: {0}", message);
    }
}

You might then modify PaymentService to create instances of both FileLogger and DatabaseLogger:

public class PaymentService
{
    private readonly FileLogger _fileLogger = new FileLogger();
    private readonly DatabaseLogger _databaseLogger = new DatabaseLogger();

    public void LogToFile(string message)
    {
        _fileLogger.Log(message);
    }

    public void LogToDatabase(string message)
    {
        _databaseLogger.Log(message);
    }
}

However, this design is not flexible. If you later need to log to EventLog or another target, you'd have to modify the PaymentService class each time, which is cumbersome and makes maintaining the code difficult.

Adding Flexibility with an Interface

To make the design more flexible, you can introduce an interface that the concrete logging classes implement. Below is an ILogger interface and its implementation in FileLogger and DatabaseLogger.

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

public class FileLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine("Inside Log method of FileLogger.");
        LogToFile(message);
    }

    private void LogToFile(string message)
    {
        Console.WriteLine("Method: LogToFile, Text: {0}", message);
    }
}

public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine("Inside Log method of DatabaseLogger.");
        LogToDatabase(message);
    }

    private void LogToDatabase(string message)
    {
        Console.WriteLine("Method: LogToDatabase, Text: {0}", message);
    }
}

Now, you can change the concrete logger implementation in the PaymentService class more easily.

public class PaymentService
{
    public void Log(string message)
    {
        ILogger logger = new FileLogger();
        logger.Log(message);
    }
}

But the design still lacks flexibility. If you want to switch from FileLogger to DatabaseLogger, you would need to modify the PaymentService class.

Making the Design Flexible with Dependency Injection

To improve flexibility, you can use Inversion of Control (IoC) and Dependency Injection (DI). With DI, you can pass the logger instance to PaymentService using constructor injection.

public class PaymentService
{
    private readonly ILogger _logger;

    public PaymentService(ILogger logger)
    {
        _logger = logger;
    }

    public void Log(string message)
    {
        _logger.Log(message);
    }
}

Now, you can pass the concrete logger implementation when creating an instance of PaymentService:

static void Main(string[] args)
{
    ILogger logger = new FileLogger();
    PaymentService paymentService = new PaymentService(logger);
    paymentService.Log("Payment processed successfully!");
}

With this approach, the PaymentService class no longer creates the ILogger instance or decides which logger to use. This inverts the control of object creation and makes the application more flexible.

Benefits of IoC and DI

Inversion of Control and Dependency Injection offer automatic object instantiation and lifecycle management. ASP.NET Core provides a built-in IoC container for simple needs, but you can also use third-party containers for more advanced features.