This tip provides a solution for injecting optional dependencies in C# using the Microsoft Dependency Injection container.
Introduction
Suppose we need to inject an optional dependency:
public class CreateUserHandler
{
private readonly IValidator<CreateUserRequest> _validator;
private readonly IUsersRepository _usersRepository;
public CreateUserHandler(IUsersRepository usersRepository,
IValidator<CreateUserRequest> validator = null)
{
_validator = validator;
_usersRepository = usersRepository;
}
public async Task<CreateUserResponse> Handle(CreateUserRequest request)
{
_validator?.ValidateAndThrow(request);
var user = new User { Name = request.Name };
await _usersRepository.Create(user);
return new() { CreatedSuccesfully = true };
}
}
In the snippet above, validator
is our optional dependency. What we are trying to accomplish is the following: If IValidator<CreateUserRequest>
is registered in the DI container, then inject it so that we can use it, otherwise just pass null
.
If we try to run the code as it is using the Microsoft Depdendency Injection, we get a runtime error. Can we still achieve something similar?
Of course!
Solution
First, create an interface that will represent our OptionalDependency
:
public interface IOptionalDependency<T>
{
T? Value { get; }
}
Then create a concrete implementation that will retrieve the value using the serviceProvider
instance. GetService
returns default(T)
if the service is not registered.
public class OptionalDependency<T> : IOptionalDependency<T>
{
public OptionalDependency(IServiceProvider serviceProvider)
{
Value = serviceProvider.GetService<T>();
}
public T? Value { get; }
}
Then we have to register the open generic IOptionalDependency<>
in the container:
services.AddTransient(typeof(IOptionalDependency<>), typeof(OptionalDependency<>));
Finally, we have to inject our optional dependency in the handler.
public class CreateUserHandler
{
private readonly IValidator<CreateUserRequest>? _validator;
private readonly IUsersRepository _usersRepository;
public CreateUserHandler(IUsersRepository usersRepository,
IOptionalDependency<IValidator<CreateUserRequest>> validator)
{
_validator = validator.Value;
_usersRepository = usersRepository;
}
public async Task<CreateUserResponse> Handle(CreateUserRequest request)
{
_validator?.ValidateAndThrow(request);
var user = new User { Name = request.Name };
await _usersRepository.Create(user);
return new() { CreatedSuccesfully = true };
}
}
We can also use some libraries such as Optional, to be more explicit that the Value
can also not be there. The code should now look something like:
public interface IOptionalDependency<T>
{
Option<T> Value { get; }
}
public class OptionalDependency<T> : IOptionalDependency<T>
{
public OptionalDependency(IServiceProvider serviceProvider)
{
Value = serviceProvider.GetService<T>().SomeNotNull();
}
public Option<T> Value { get; }
}
public class CreateUserHandler
{
private readonly Option<IValidator<CreateUserRequest>> _validatorOption;
private readonly IUsersRepository _usersRepository;
public CreateUserHandler(IUsersRepository usersRepository,
IOptionalDependency<IValidator<CreateUserRequest>> validator)
{
_validatorOption = validator.Value;
_usersRepository = usersRepository;
}
public async Task<CreateUserResponse> Handle(CreateUserRequest request)
{
_validatorOption.MatchSome(validator => validator.ValidateAndThrow(request));
var user = new User { Name = request.Name };
await _usersRepository.Create(user);
return new() { CreatedSuccesfully = true };
}
}
History
- 11th June, 2023: Initial version