simpleinjector / SimpleInjector.Integration.AspNetCore

MIT License
2 stars 3 forks source link

Add support for [FromServices] attribute #22

Open dotnetjunkie opened 3 years ago

dotnetjunkie commented 3 years ago

ASP.NET Core supports method injection in its MVC controllers through the use of the FromServices attribute. Simple Injector does not support this, and there is currently no intention in supporting this. This issue is merely a description on what it takes to add support for this in the ASP.NET Core integration package.

Things to take into consideration:

ASP.NET Core will, by default, resolve a [FromServices] dependency from the built-in configuration system—not from Simple Injector. These calls can be intercepted by replacing ASP.NET Core's Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ServicesModelBinderProvider. A possible implementation might look as follows:

public class SimpleInjectorServicesModelBinderProvider : IModelBinderProvider
{
    private readonly SimpleInjectorServicesModelBinder modelBinder;

    public SimpleInjectorServicesModelBinderProvider(Container container)
    {
        this.modelBinder = new SimpleInjectorServicesModelBinder(container);
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.BindingInfo.BindingSource != null &&
            context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Services))
        {
            return modelBinder;
        }

        return null;
    }

    private sealed class SimpleInjectorServicesModelBinder : IModelBinder
    {
        private readonly Container container;

        public SimpleInjectorServicesModelBinder(Container container)
        {
            this.container = container;
        }

        /// <inheritdoc />
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var model = this.container.GetInstance(bindingContext.ModelType);

            bindingContext.ValidationState.Add(
                model, new ValidationStateEntry() { SuppressValidation = true });

            bindingContext.Result = ModelBindingResult.Success(model);
            return Task.CompletedTask;
        }
    }
}

This custom binder provider can replace the built-in one:

mvcOptions.ModelBinderProviders.RemoveType<ServicesModelBinderProvider>();
mvcOptions.ModelBinderProviders.Add(new SimpleInjectorServicesModelBinderProvider(container));

Limitations of this implementation:

This is part of the solution. What's still missing here is the integration with the diagnostics subsystem. This is likely something that should be done inside the AddControllerActivation() extension method. This likely involves the registration of a ExpressionBuilding or ExpressionBuilt event, because this is the interception point that allows informing Simple Injector about known dependencies.

Resources:

davidroth commented 2 years ago

What are your thoughts about supporting "minimal api" service injection with SimpleInjector? Minimal api routig will probably get a commonly used alternative to controllers. And the [FromServices] attribute isnt even required anymore because internally the framework checks if arguments are registered services by using the IServiceProviderIsService interface (Docs). It is still possible to use FromServices to declare the binding decisions explicitly. But using it is optional. See parameter binding docs.

So does this issue also cover minimal apis and are minimal api endpoints without FromServices attribute a dead-end for SimpleInjector?

dotnetjunkie commented 2 years ago

I've been reading through the MS docs on minimal APIs to understand what it is. I'm not sure I fully support the idea, but that's besides the point.

Unfortunately, as far as I can see by looking at the .NET 6 code base, any interception point that allows resolving any service argument from Simple Injector is missing. The docs contain the following example:

app.MapGet("/{id}", (int id, int page, Service service) => { });

This allows Service to be resolved in case it is registered in MS.DI. But this is hard-wired to the built-in container. As Simple Injector is a 'non-conforming' container, the framework must have a hook that would allow Simple Injector to intercept the call to resolve Service. But after inspecting the source code of EndpointRouteBuilderExtensions and RequestDelegateFactory, I have to conclude any required hooks are missing. For the earlier mentioned [FromServices] in MVC action methods, IModelBinderProvider functions as possible interception point, but for Minimal-API registered delegates, such interception point seems to be missing. This disallows a method such as the above where Simple Injector-registered services are added to the mapped delegate definition.

A model, however, that I've been promoting for a number of years now has many similarities to the new .NET 6 minimal API model. This model is described in the SOLID Services POC on GitHub. In SOLID services, instead of relying on manually defined mappings using app.MapGet(...) (as the above code snippet shows), the API is defined by the specified query and command objects. Using reflection, a similar mapping is made that maps an incoming request to an underlying handler. This solution uses Simple Injector to demonstrate the concept.

From a Minimal API perspective, the SOLID Services 'map' requests similar to what would be the following with Minimal API:

app.MapGet("/api/queries/GetOrderById/{OrderId}", (int OrderId) => (OrderInfo)null);
app.MapGet("/api/queries/GetUnshippedOrdersForCurrentCustomer", (GetUnshippedOrdersForCurrentCustomerQuery query) => (Paged<OrderInfo>)null);
app.MapPost("/api/commands/CreateOrder", (CreateOrderCommand command) => { });
app.MapPost("/api/commands/ShipOrder", (ShipOrderCommand command) => { });

The SOLID Services project contains an ASP.NET Core example, although it uses an ASP.NET Core 3.1. It might be useful to add an example project that plugs in into the new Minimal API structure, because it likely gives many advantages (such as generation of API documentation, which the current 3.1 project doesn't support).

dotnetjunkie commented 2 years ago

@davidroth,

Even better, the SOLID Services now contains an example project that uses ASP.NET Core 6 Minimal API, which actually drastically simplifies the amount of code needed to wire this up.

Without the optional Swagger configuration, the Program file is not much more than this:

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

services.AddSimpleInjector(container, options =>
{
    options.AddAspNetCore();
});

Bootstrapper.Bootstrap(container);

var app = builder.Build();

app.MapCommands("/api/commands/{0}", container, Bootstrapper.GetKnownCommandTypes());
app.MapQueries("/api/queries/{0}", container, Bootstrapper.GetKnownQueryTypes());

app.Run();

Here MapCommands and MapQueries are custom extension methods that iterate the list of known command and query types and do the appropriate app.MapPost(...) calls.

This model ties completely into the ASP.NET Core pipeline, but with all the advantages that the ICommandHandler<T> and IQueryHandler<TQuery, TResult> bring to the table.

davidroth commented 2 years ago

Thanks for your thoughts steven! The Solid services example is nice. Unfortunately the query string issue is a real blocker in many scenarios. Beside from that, it would still be nice to have the option to use lambdas and SimpleInjector without the dynamic command dispatch helpers. But I see the problem when RequestDelegateFactory is once again hard coded against ServiceProvider :-/

dotnetjunkie commented 2 years ago

Unfortunately the query string issue is a real blocker in many scenarios.

I accept pull requests ;-)

dotnetjunkie commented 2 years ago

Last week I tried to implement a more 'REST-full' method making use of the new .NET 6 Map API, but this proved to be extremely difficult. That new API analyzes the structure (input and return parameters) of the supplied delegate and uses this to construct a route and build an explorable API.

Because I'd like messages to be 'batch-registered' in the API, those delegates need to be auto-generated. Supplying a compiled expression, unfortunately, breaks the Map API, because compiled expressions lack parameter names and don't allow retrieving their custom attributes. An alternative is using LCG to generate classes with methods dynamically, but this is an awful lot of work, and was a path I stopped pursuing.

Two alternative remain: implementing this through source generators or providing Microsoft with a feature and pull request in the hope that a future version of Minimal API gives more flexibility.