How to use inversion of control in C#
By FoxLearn 1/3/2025 6:49:11 AM 67
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.
- How to fix 'Failure sending mail' in C#
- How to Parse a Comma-Separated String from App.config in C#
- How to convert a dictionary to a list in C#
- How to retrieve the Executable Path in C#
- How to validate an IP address in C#
- How to retrieve the Downloads Directory Path in C#
- C# Tutorial
- Dictionary with multiple values per key in C#