Dependency Injection In C# & .NET Part 8 - Types Of Dependency Injection
[C#, .NET, Architecture, Domain Design]
This is Part 8 of a series on dependency injection.
- Dependency Injection In C# & .NET Part 1 - Introduction & Basic Implementation
- Dependency Injection In C# & .NET Part 2 - Making Implementations Swappable
- Dependency Injection In C# & .NET Part 3 - Making Implementations Pluggable
- Dependency Injection In C# & .NET Part 4 - Making Implementations Hot-Pluggable
- Dependency Injection In C# & .NET Part 5 - Making All Implementations Available
- Dependency Injection In C# & .NET Part 6 - Implementation Testing
- Dependency Injection In C# & .NET Part 7 - Integration Testing
- Dependency Injection In C# & .NET Part 8 - Types Of Dependency Injection (this post)
- Dependency Injection In C# & .NET Part 9 - Life Cycles
- Dependency Injection In C# & .NET Part 10 - Conclusion
In our last post, we discussed how to use dependency injection for testing. In this post, we will examine the three different types of dependency injection.
Constructor Injection
The first type is constructor injection. Here, the dependency is passed through the constructor during object creation. This is generally the most common type, and throughout our series of posts, this is, in fact, the type we used.
Passing the dependency through the constructor has the following implications:
- The dependency is mandatory
- The dependency is typically immutable
- The dependency is immediately available after the object has been instantiated
For example, let us say we want to create a GeneralAlertSenderAlpha
and inject an AlertSender
through the constructor.
Our class would look like this:
public class GeneralAlertSenderAlpha
{
private readonly IAlertSender _alertSender;
public GeneralAlertSenderAlpha(IAlertSender alertSender)
{
_alertSender = alertSender;
}
public async Task<string> SendAlert(string title, string message)
{
return await _alertSender.SendAlert(new GeneralAlert(title, message));
}
}
And we would instantiate it like this:
var sender = new GeneralAlertSenderAlpha(zohoAlertSender);
var result = await sender.SendAlert(genericAlert.Title, genericAlert.Message);
As indicated earlier:
- You must supply an
AlertSender
during the instantiation of this object - Once you have supplied the
AlertSender
, you (generally) cannot change it
The benefits of this type of injection are:
- The class is easy to test
- The requirements are explicit
- By reading the code, it is easy to understand how to instantiate the class and how it works
The main drawbacks are:
- When there are many types to be injected, the constructor can become very unwieldy and verbose
- Additionally, when there are many injected types, the object can be expensive to instantiate
Property Injection
This type of injection is also called setter injection.
This type provides dependencies via public properties after the object has been instantiated.
public class GeneralAlertSenderBeta
{
public IAlertSender? AlertSender { get; set; }
public async Task<string> SendAlert(string title, string message)
{
if (AlertSender is null)
throw new Exception("AlertSender is mandatory!");
return await AlertSender!.SendAlert(new GeneralAlert(title, message));
}
}
Passing a dependency as a property has the following implications:
- Their instantiation can be deferred until they are needed after the construction of the object
- They can be swapped out when needed during the lifetime of the class
We can implement this as follows:
public class GeneralAlertSenderBeta
{
public IAlertSender? AlertSender { get; set; }
public async Task<string> SendAlert(string title, string message)
{
if (AlertSender is null)
throw new Exception("AlertSender is mandatory!");
return await AlertSender!.SendAlert(new GeneralAlert(title, message));
}
}
We can then use it as follows:
// Set the sender
senderBeta.AlertSender = zohoAlertSender;
var betaResult = await senderBeta.SendAlert(genericAlert.Title, genericAlert.Message);
// Reset the sender
senderBeta.AlertSender = office365AlertSender;
betaResult = await senderBeta.SendAlert(genericAlert.Title, genericAlert.Message);
Benefits:
- Object construction is cheap, as the dependencies are not needed upfront
- The dependencies can be swapped out after object construction
- The dependencies do not need to be known up front
- Once set, the dependency can be reused
Drawbacks:
- It is easy to forget to set them before usage, and therefore, the code must check that they are instantiated first before use
- Testing is a bit more complicated
Method Injection
The final type is known as method injection. Here, the dependency is passed to the method that requires its use.
The implications of this are as follows:
- The dependency is injected during its invocation, together with any (if required) parameters
- The object (generally) has no access to the inner state of the dependency.
It is implemented as follows:
public class GeneralAlertSenderCharlie
{
public async Task<string> SendAlert(IAlertSender alertSender, string title, string message)
{
return await alertSender.SendAlert(new GeneralAlert(title, message));
}
}
And is used as follows:
// Create the charlie result
var senderCharlie = new GeneralAlertSenderCharlie();
// Send using Zoho
var charlieResult = await senderCharlie.SendAlert(zohoAlertSender, genericAlert.Title, genericAlert.Message);
// Send using Office
charlieResult = await senderCharlie.SendAlert(office365AlertSender, genericAlert.Title, genericAlert.Message);
Benefits:
- Objects and their dependencies are completely decoupled
- Different dependencies can be used at the point of invocation during the lifetime of the class
Drawbacks:
- Testing is more complicated
- The fact that the dependency can be mutated elsewhere makes behaviour difficult to predict.
In our next post, we will look at dependency injection life cycles.
The code is in my GitHub. The source code builds from first principles as outlined in this series of posts with different versions of the API demonstrating the improvements.
Happy hacking!