Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Optional DependencyInjection in .NET

4.29/5 (8 votes)
11 Jun 2023CPOL 18.2K  
How to optionally inject a dependency using Microsoft DependencyInjection container
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:

C#
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:

C#
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.

C#
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:

C#
services.AddTransient(typeof(IOptionalDependency<>), typeof(OptionalDependency<>));

Finally, we have to inject our optional dependency in the handler.

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)