simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 154 forks source link

Initegration with Mimimal API Library #997

Closed dermaine-learn closed 4 months ago

dermaine-learn commented 4 months ago

hi,

I started migrating an existing API to use the new Minimal APIs in AP.NET Core. I am using the library below for better organizing my API's and was having issues using Simple Injector to inject dependencies.

https://github.com/michelcedric/StructuredMinimalApi

I am trying to find a way to use Simple Injector to intercept and resolve dependencies for types implementing IEndpoint interface.

Is there an efficient way for handing this using simple injector. I also saw your recommendation in issue. Would it be a similar approach?

Thanks in advance.

dotnetjunkie commented 4 months ago

The referenced StructuredMinimalApi repository provides the following example:

public class GetWithParamEndpoint : IEndpoint<string, string>
{
    public void AddRoute(IEndpointRouteBuilder app)
    {
        app.MapGet("/Todo/2/{param1}", (string param1) =>
            HandleAsync(param1));
    }

    public Task<string> HandleAsync(string request)
    {
        return Task.FromResult($"Hello World! 2 {request}");
    }
}

As long as you stick with that example, there is simply no way to intercept the call to HandleAsync. Decorating an IEndpoint<TRequest, TResponse> has no effect, because the HandleAsync method is never called through the IEndpoint interface. As the example shows, the HandleAsync method is called from within the AddRoute method. This tightly couples the method implementation to the app.MapGet registration. Interception through decoration will have no effect whatsoever.

Please also note that there is a design issue with this library especially concerning the injection of Scoped dependencies into endpoints. I just reported this here.

If you were to do this, you are better of defining your endpoint classes as Humble Objects instead. That means, doing the least amount of code in that class, which should only consist of infrastructure code, and extract any relevant business logic out of the endpoint, into a new class, and wrap that class behind and abstraction. That abstraction can than be decorated.

For instance, you can define the following abstractions:

public interface IRequest<TResponse> { }
public interface IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse> { }
public interface IMessageDispatcher
{
    Task<TResponse> HandleAsync<TResponse>(IRequest<TResponse> req);
}

With that, you can create a HelloWorld request message and HelloWolrdHandler class:

public record HelloWorld(string Request) : IRequest<string> { }

public class HelloWorldHandler : IRequestHandler<HelloWorld, string>
{
    public Task<string> HandleAsync(HelloWorld request)
    {
        return Task.FromResult($"Hello World! 2 {request.Request}");
    }
}

These handlers can be registered in Simple Injector as follows:

container.Register(typeof(IRequestHandler<,>),
    AppDomain.CurrentDomain.GetAssemblies());

This would allow you to simplify your endpoint classes to the following:

public record GetWithParamEndpoint(IMessageDispatcher Dispatcher)
    : IEndpoint
{
    public void AddRoute(IEndpointRouteBuilder app)
    {
        app.MapGet("/Todo/2/{param1}",
            (string param1) => this.Dispatcher.HandleAsync(
                new HelloWorld(param1)));
    }
}

This endpoint class makes use of the IMessageDispatcher. It will be responsible for accepting any arbitrary IRequest<TResponse> instances and forwarding it to the correct underlying IRequestHandler<TRequest, TResponse> implementation. You'll need a custom Simple Injector-specific implementation for this:

record SimpleInjectorMessageDispatcher(Container Container)
    IMessageDispatcher
{
    public Task<TResponse> HandleAsync<TResponse>(
        IRequest<TResponse> request)
    {
        var handlerType = typeof(IRequestHandler<,>)
            .MakeGenericType(request.GetType(), typeof(TResponse));

        dynamic handler = container.GetInstance(handlerType);

        return handler.HandleAsync((dynamic)request);
    }
}

This dispatcher can be registered as follows:

// Add to MS.DI
service.AddSingleton<IRequestDispatcher>(
    new SimpleInjectorRequestDispatcher(container));

This wired the complete infrastructure together. With this, you can now wrap decorators around IRequestHandler<,> implementations:

container.RegisterDecorator(
    typeof(IRequestHandler<,>),
    typeof(LoggingRequestHandlerDecorator<,>));

I hope this helps.

dermaine-learn commented 4 months ago

hi dotnetjunkie,

Thanks for your very detail response. This helps a ton.

I have a question based on the issue you submitted for the library, the issue with the scope not being disposed is still a problem correct? Is it just a matter of registering the endpoints as singletons and MapEndpoinst updated to match below?

public static class IEndpointRouteBuilderExtensions
{
    public static void MapEndpoints(this WebApplication builder)
    {
        var endpoints = builder.Services.GetServices<IEndpoint>();

        foreach (var endpoint in endpoints)
        {
            endpoint.AddRoute(builder);
        }
    }
}

Thanks in advance.

dotnetjunkie commented 4 months ago

Is it just a matter of registering the endpoints as singletons and MapEndpoinst updated to match below?

Well... it depends. This prevents dependencies not being disposed when the application stops and undetected problems due to captive dependencies. It still prevents scoped dependencies (such as DbContext instances) from being injected into your endpoints though. But when you use the endpoints as Humble Objects, this will not be limiting restriction.

dermaine-learn commented 4 months ago

Thanks for clarifying, I will use the endpoint as humble object approach along with the other recommenadations your listed: Register Endpoints as singletons Resolve endpoints using container root.

Also sorry for repeating above, I went back through issue you raised and relaized you already included the above block for resolving using root container in MapEndpoints.

Thanks for your speedy response as usual.