The following are some of the patterns available that can be used for DI:
- Constructor injection
- Property injection
- Method injection
- Service Locator
The following section provides details on these various patterns.
Constructor injection
Constructor injection is a technique in which dependencies are passed through a class's constructor. This is an excellent way to inject dependencies because the dependencies are made explicit. The object cannot be instantiated without its dependencies.
If a class can be instantiated and its methods called, but the functionality does not work properly because one or more dependencies have not been provided, classes are being dishonest with their clients. It is a better practice to explicitly require dependencies.
The following example shows the constructor injection pattern:
public class Employee : Person
{
private readonly ILogger _logger;
private readonly ICache _cache;
// Dependencies are injected via the constructor,
// including the base class
public Employee(ILogger logger, ICache cache,
IOrgService orgService)
: base(logger, orgService)
{
if (logger == null)
throw new ArgumentNullException(nameof(logger));
if (cache == null)
throw new ArgumentNullException(nameof(cache));
_logger = logger;
_cache = cache;
}
}
public class Person
{
private readonly ILogger _logger;
private readonly IOrgService _orgService;
// Dependencies are injected via the constructor
public Person(ILogger logger, IOrgService orgService)
{
if (logger == null)
throw new ArgumentNullException(nameof(logger));
if (orgService == null)
throw new ArgumentNullException(nameof(orgService));
_logger = logger;
_orgService = orgService;
}
}
In this example, you can see that the Employee class has three dependencies: instances of ILogger, ICache, and IOrgService. The Person class, which is the base class of Employee, has two dependencies: instances of ILogger and IOrgService.
Constructor injection is used to provide instances of all of the dependencies for both classes. In this example, notice that the Employee class passes the dependencies that the Person class needs to it. Both the Employee and the Person classes have a dependency to ILogger and only the Employee class has a dependency to ICache. The Employee class doesn't even use the orgService instance directly, which is why it is not assigned to a class-level variable, but since its base class needs it, it is a dependency that is injected in and then passed on to its base class (Person).
The dependencies that are injected in are assigned to readonly variables. In the C# language, the readonly keyword indicates that the field can only be assigned as part of its declaration or in the constructor. We will not be assigning an instance of the dependency anywhere else other than the constructor with constructor injection, so it can be marked as readonly.
There is a guard clause that ensures that the required dependencies are not null. If they are, an exception is thrown. If a valid instance is passed in, it is assigned to a private variable so that the instance can be used later in the logic of the class.
Property injection
Property injection allows clients to supply a dependency through a public property. If you need to provide callers with the ability to provide an instance of a dependency, such as to override the default behavior, you can use this injection pattern.
The first time the getter is called, if the dependency has not already been supplied, a default instance should be provided through lazy initialization:
public class Person
{
private IOrgService _orgService;
public IOrgService OrgService
{
get
{
if (_orgService == null)
{
// Lazy initialization of default
_orgService = new OrgService();
}
return _orgService;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
// Only allow dependency to be set once
if (_orgService != null)
{
throw new InvalidOperationException();
}
_orgService = value;
}
}
}
If you want a dependency to only be supplied once through a setter, a check can be performed in the setter, as is done in the example.
For the lazy initialization, you will need a way to get the default instance. It is not ideal to instantiate, or new up, the dependency in the class. However, the default value could be provided by another means, such as constructor injection. The property would give you the flexibility to provide a different instance at runtime.
The property injection pattern provides dependencies in an implicit way. If a default instance is not provided in the getter when the dependency has not been previously set and the logic needs the dependency to work properly, an error or unexpected results could occur. If you are going to use this pattern over an explicit one, such as constructor injection, a default instance should be provided in some way.
Unlike the example for constructor injection, notice that the private class-level variable to hold the dependency (_orgService) does not have the readonly keyword. In order to use property injection, we need the ability to set the variable outside of the variable's declaration and the constructor.
Method injection
Method injection is similar to property injection, except a dependency is provided through a method rather than a property:
public class Person
{
private IOrgService _orgService;
public void Initialize(IOrgService orgService)
{
if (orgService == null)
throw new ArgumentNullException(nameof(orgService));
_orgService = orgService;
}
}
Service LocatorĀ
The service locator pattern uses a locator object that encapsulates logic to determine and provide an instance of the dependencies that are needed. Although it will vary depending on your implementation, a sample call with a service locator might look something like the following, resulting in an instance being provided based on the specified interface:
var cache = ServiceLocator.GetInstance<ILogger>();
Using the service locator pattern to get dependencies is considered to be an anti-pattern by some people because it hides a class's dependencies. As opposed to constructor injection, where we can see the dependencies in the public constructor, we would have to look at the code to find dependencies being resolved through the service locator. Hiding dependencies in this way can lead to runtime or compile-time issues, and make it more difficult to reuse the code. This is particularly true if we do not have access to the source code, which might be the case if we are using code from a third party. It is preferable to use an explicit method to acquire an instance of a dependency.