1. Dependency Injection

Dependency Injection

Dependency Injection

There are three different ways to get access to services registered in the IOC container. Consider the following service registration.

HelloWorldService.cs
public interface IHelloWorldService
{
    string SayHello();
}

public class HelloWorldService : IHelloWorldService
{
    public string SayHello() => "hello world!";
}

IOC Registration

Program.cs
builder.Services.AddScoped<IHelloWorldService, HelloWorldService>();

Property Injection

Services can be automatically property injected by simply adding properties to the endpoint like so:

MyEndpoint.cs
public class MyEndpoint : EndpointWithoutRequest
{
    public IHelloWorldService HelloService { get; set; }

    public override void Configure()
    {
        Get("/api/hello-world");
    }

    public override async Task HandleAsync(CancellationToken ct)
    {
        await SendAsync(HelloService.SayHello());
    }
}

Constructor Injection

Constructor injection is also supported. Just make sure not to assign the injected dependencies to public properties if using together with property injection.

MyEndpoint.cs
public class MyEndpoint : EndpointWithoutRequest
{
    private IHelloWorldService _helloService;

    public MyEndpoint(IHelloWorldService helloScv)
    {
        _helloService = helloScv;
    }

    public override void Configure()
    {
        Get("/api/hello-world");
    }

    public override async Task HandleAsync(CancellationToken ct)
    {
        await SendAsync(_helloService.SayHello());
    }
}

Manual Resolving

Services can be resolved manually like so:

public override async Task HandleAsync(CancellationToken ct)
{
    IHelloWorldService? helloSvc = TryResolve<IHelloWorldService>();

    if (helloSvc is null)
        ThrowError("service not resolved!");

    var logger = Resolve<ILogger<MyEndpoint>>();

    logger.LogInformation("hello service is resolved...");

    await SendAsync(helloSvc.SayHello());
}
  • TryResolve() - This method will try to resolve the given service. returns null if not resolved.

  • Resolve() - This method will throw an exception if the requested service cannot be resolved.

Pre-Resolved Services

The following services are pre-resolved and available for every endpoint handler with the following properties:

property: Config
service : IConfiguration

property: Env
service : IWebHostEnvironment

property: Logger
service : ILogger

They can be used in the endpoint handlers like so:

public override async Task HandleAsync(CancellationToken ct)
{
    Logger.LogInformation("this is a log message");
    var isProduction = Env.IsProduction();
    var smtpServer = Config["SMTP:HostName"];
}

Validator Dependencies

Validators are singletons for performance reasons. Constructor injection as well as the above-mentioned *Resolve() methods are available for validators to get access to the dependencies. Take care not to maintain state in the validator unless that data is also singleton/static.

Trying to resolve a scoped dependency in the constructor will cause an error to be thrown. Scoped dependencies can be resolved in the constructor by creating a new scope and disposing it as shown below:

MyValidator.cs
public class MyValidator : Validator<Request>
{
    public MyValidator(ISomeSingletonService singletonService)
    {
        var singletonSvc = singletonService; //from ctor injection
        var anotherSingletonService = Resolve<IAnotherSingletonService>(); //manual resolving

        //scoped dependency resolving
        using var scope = CreateScope();
        var scopedService = scope.Resolve<ISomeScopedService>();
    }
}

Most importantly don't forget the using keyword or manually dispose the scope at the end.

You can however call *Resolve() when needing to access scoped dependencies inside the rules without having to create a new scope on each execution. The same scope as the current http context scope is used in this case.

public class MyValidator : Validator<Request>
{
    public MyValidator()
    {
        RuleFor(x => x.Id).MustAsync(async (id, ct) =>
        {
            var idChecker = Resolve<IScopedService>();
            return await idChecker.IsValidId(id, ct);
        });
    }
}

Entity Mapper Dependencies

Just like validators, mappers are also singletons and you need to create a scope and dispose it if you need to resolve scoped dependencies in the constructor.

MyMapper.cs
public class MyMapper : Mapper<Request, Response, Person>
{
    public MyMapper(ISomeSingletonService singletonService)
    {
        var singletonSvc = singletonService; //from ctor injection
        var anotherSingletonService = Resolve<IAnotherSingletonService>(); //manual resolving

        //scoped dependency resolving
        using var scope = CreateScope();
        var scopedService = scope.Resolve<ISomeScopedService>();
    }
}

Non-constructor methods can call *Resolve() when needing to access scoped dependencies without having to create a new scope on each execution. The same scope as the current http context scope is used.

public class Mapper : Mapper<Request, Response, Person>
{
    public override Task<Person> ToEntityAsync(Request r)
    {
        var db = Resolve<DBContext>();
        return db.GetPerson(r.Id);
    }
}

Pre/Post Processor Dependencies

Processors are also singletons and can only get dependencies out of the DI container via the HttpContext. Constructor injection is not supported.

MyRequestLogger.cs
public class MyRequestLogger<TRequest> : IPreProcessor<TRequest>
{
    public Task PreProcessAsync(TRequest req, HttpContext ctx, List<ValidationFailure> failures, CancellationToken ct)
    {
        var logger = ctx.Resolve<ILogger<TRequest>>();
    }
}

Event Handler Dependencies

Event handlers are also singletons and they only support constructor injection. Since the constructor is only executed once, you must inject an IServiceScopeFactory and use that to create a scope in order to resolve scoped dependencies in the handle method. Singleton dependencies can be injected as usual in the constructor.

MyEventHandler.cs
public class MyEventHandler : IEventHandler<MyEvent>
{
    private readonly IServiceScopeFactory _scopeFactory;

    public MyEventHandler(IServiceScopeFactory scopeFactory, ILogger<MyEventHandler> logger)
    {
        _scopeFactory = scopeFactory;
        logger.LogInformation("this will only be logged once");
    }

    public async Task HandleAsync(MyEvent eventModel, CancellationToken ct)
    {
        using var scope = _scopeFactory.CreateScope();
        var service = scope.Resolve<IScopedService>();
        await service.DoSomething();
    }
}

Command Handler Dependencies

Unlike event handlers, command handlers are not singletons and the constructor is executed per each command execution. Scoped dependencies can therefore simply be injected using the constructor and used inside the execute method like so:

public class MyCommandHandler : ICommandHandler<MyCommand, string>
{
    private readonly IScopedService _scopedService;

    public MyCommandHandler(IScopedService scopedService, ILogger<MyCommandHandler> logger)
    {
        _scopedService = scopedService;
        logger.LogInformation("this will logged on each execution");
    }

    public async Task<string> ExecuteAsync(MyCommand command, CancellationToken ct)
    {
        return await _scopedService.GetSomething();
    }
}
NOTE

If the command handler execution happens outside the duration of an http request and it needs to access a scoped dependency, an exception will be thrown. In such situations you can inject an IServiceScopeFactory in the constructor and create a scope as described in the event handler section above. I.e. the need for a scope factory would only arise if/when you execute commands outside of endpoints.


© FastEndpoints 2022