Open dotnetjunkie opened 3 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?
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).
@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.
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 :-/
Unfortunately the query string issue is a real blocker in many scenarios.
I accept pull requests ;-)
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.
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:
[FromServices]
-marked method argument, will always be resolved from the ASP.NET Core configuration system; not from Simple Injector.[FromServices]
-marked method argument is aKnownDependency
of the specific controller. This allows object graphs to be visualized that include those services and analysis to be performed based on the relationship of the method-injected dependency with its consuming controller.Index([FromServices]ILogger logger)
method on anHomeController
, whould ideally get anLogger<HomeController>
injected in case the user uses Simple Injector's.AddLogging()
extension method. This might not be possible, but further investigation is required.[FromServices]
attribute. A decision needs to be made whether counting of dependencies should be limited to constructors or not. If so, a change to the core library is required, separating dependency types (e.g. constructor, property, method, other).[FromServices]
dependency is not registered, verification should fail.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'sMicrosoft.AspNetCore.Mvc.ModelBinding.Binders.ServicesModelBinderProvider
. A possible implementation might look as follows:This custom binder provider can replace the built-in one:
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 aExpressionBuilding
orExpressionBuilt
event, because this is the interception point that allows informing Simple Injector about known dependencies.Resources: