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 155 forks source link

Integration with FastEndpoints #955

Closed nhaberl closed 1 year ago

nhaberl commented 2 years ago

I would like to use https://fast-endpoints.com but have wired up SimpleInjector for my business logic a lot so my question is if minimal APIs are supported without any hassle ? I am asking because of https://github.com/dotnet/aspnetcore/issues/41863 and https://github.com/FastEndpoints/Library/issues/218

dotnetjunkie commented 2 years ago

Short answer is: nope. The design of Minimal API is tightly coupled to the built-in DI system and it doesn't allow "non conformers" such as Simple Injector to hook into that pipeline easily.

I would, therefore, advise an application architecture that makes it easy to forward calls back to Simple Injector. This can be done, for instance, by designing the application around message-based patterns, such as the command handlers and query handlers I described on my blog.

This makes mapping an incoming API call to a handler -provided by Simple Injector- fairly easy. You can either call the container from within your function -or- you can register a mediator class in the built-in container that depends on Simple Injector and forwards the call.

I hope this helps

nhaberl commented 2 years ago

Thanks a lot and yes I do that but want to be sure that I can use FastEndpoints and Injecting my handlers there. Otherwise I will stay with controllers (and no minimal APIs) which should work as is ? And maybe you could provide a sample within your docs for ASP.NET 6 guys ...

dotnetjunkie commented 2 years ago

Otherwise I will stay with controllers (and no minimal APIs) which should work as is ?

Absolutely. Just follow the guidance from the documentation

And maybe you could provide a sample within your docs for ASP.NET 6 guys ..

Good point. Should certainly be documented.

BTW, you might be interested in the SOLID Services implementation using Minimal API. It can benfound here: https://github.com/dotnetjunkie/solidservices/blob/master/src/WebCore6Service/Program.cs

nhaberl commented 2 years ago

One more thing please, does SI request scope guarantee that there is only one instance per request ? Because I am queuing commands during request and commit them at the end so my queue should only be valid for this scope.

Dont know why Microsoft acts so differently there.

So does SI always respect the scope no matter where it gets called ?

dotnetjunkie commented 2 years ago

I'm unsure I understand your question. Can you give an example? How does MS.DI act different?

dotnetjunkie commented 1 year ago

In the meantime, as long as FastEndpoints doesn't support the proper interception points to plugin to, I'd suggest adding an extra layer of abstraction that can be used to implement your endpoints. For instance:

// Single application interface for your request handlers
public interface IRequestHandler<TRequest, TResponse>
{
    Task<TResponse> HandleAsync(TRequest request, CancellationToken ct);
}

Instead of implementing the logic inside FastEndpoints derived Endpoint<T> classes, instead implement the logic inside your IRequestHandler<TRequest, TResponse> implementations. For instance:

// This class is created by Simple Injector
public sealed class MyRequestHandler : IRequestHandler<MyRequest, MyResponse>
{
    public MyRequestHandler(/* place dependencies here */)
    {
    }

    public async Task<MyResponse> HandleAsync(MyRequest req, CancellationToken ct)
    {
        return new MyResponse()
        {
            FullName = req.FirstName + " " + req.LastName,
            IsOver18 = req.Age > 18
        };
    }
}

This reduces your endpoint class to the following:

// This class is now a Humble Object and will be created using MS.DI
[HttpPost("/api/user/create")]
[AllowAnonymous]
public class MyEndpoint : Endpoint<MyRequest>
{
    private readonly IRequestHandler<MyRequest, MyResponse> handler;

    public MyEndpoint(IRequestHandler<MyRequest, MyResponse> handler) =>
        this.handler = handler;

    public override async Task HandleAsync(MyRequest req, CancellationToken ct) =>
        await SendAsync(await handler.HandleAsync(req, ct));
}

The only thing now missing is a proxy that bridges the gab between MS.DI and Simple Injector. That can be achieved using the following implementation:

// Gets constructed by MS.DI and forwards a request to a handler constructed by SI.
public sealed record SimpleInjectorRequestHandlerDispatcher<TRequest, TResponse>(
    SimpleInjector.Container Container) : IRequestHandler<TRequest, TResponse>
{
    public Task<TResponse> HandleAsync(TRequest request, CancellationToken ct) =>
        Container.GetInstance<IRequestHandler<TRequest, TResponse>>()
            .HandleAsync(request, ct);
}

And everything can be tied together as follows:

builder.Services.AddSingleton(
    typeof(IRequestHandler<,>),
    typeof(SimpleInjectorRequestHandlerDispatcher<,>));

container.Register(typeof(IRequestHandler<,>), typeof(MyRequestHandler).Assembly);

That's all. If you wish to keep all the logic together, you could also place the requesthandler class inside its corresponding endpoint.

For completeness, this would be your complete Program file:

global using FastEndpoints;

var container = new SimpleInjector.Container();

container.Register(typeof(IRequestHandler<,>), typeof(MyRequestHandler).Assembly);

var builder = WebApplication.CreateBuilder();
builder.Services
    .AddFastEndpoints()
    .AddSimpleInjector(container, _ => _.AddAspNetCore());

builder.Services.AddSingleton(
    typeof(IRequestHandler<,>),
    typeof(SimpleInjectorRequestHandlerDispatcher<,>));

var app = builder.Build();
app.UseAuthorization();
app.UseFastEndpoints();
app.UseSimpleInjector();
app.Run();
dotnetjunkie commented 1 year ago

The following PR was merged to the main branch. This likely means that the work of the PR will be available in the next minor release (v5.2) of FastEndpoints. With that release, you can use the following code to integrate Simple Injector with FastEndpoints:

using SimpleInjector;
using FastEndpoints;

var container = new Container();
var builder = WebApplication.CreateBuilder();

builder.Services
    .AddFastEndpoints()
    .AddSingleton<IEndpointFactory, SimpleInjectorEndpointFactory>()
    .AddSimpleInjector(container, options =>{ options.AddAspNetCore(); });

var endpoints = builder.Services.Where(s => typeof(BaseEndpoint).IsAssignableFrom(s.ServiceType));
foreach (var endpoint in endpoints.ToArray())
{
    // Move endpoint registration to Simple Injector
    builder.Services.Remove(endpoint);
    container.Register(endpoint.ImplementationType!);
}

var app = builder.Build();
app
    .UseAuthorization()
    .UseFastEndpoints()
    .UseSimpleInjector(container);

app.Run();

public record SimpleInjectorEndpointFactory(Container Container) : IEndpointFactory
{
    public BaseEndpoint Create(EndpointDefinition definition, HttpContext ctx) =>
        (BaseEndpoint)Container.GetInstance(definition.EndpointType);
}
nhaberl commented 1 year ago

Thank you very much !!!

ardalis commented 1 month ago

I ran into this yesterday as I was trying to add FastEndpoints to an existing ASPNET Core MVC project that was using SimpleInjector. I wasn't able to get both of them working together as I would have liked because I couldn't call .UseSimpleInjector(container) twice, once for FastEndpoints and once for Controllers. It throws an exception if it's called twice.

My current workaround was to pass Container into my new FastEndpoint and then resolve needed classes manually in the constructor. This works but is obviously not ideal.