Support BindAsync Surrogates in Minimal APIs
See original GitHub issueSummary
Support parameter binding using a surrogate type.
Motivation and goals
The existing parameter binding mechanisms are insufficient for all types of model binding. This is particularly painful for extension authors that previously relied on using IModelBinderProvider
and IModelBinder
.
As a developer, I might want:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning();
var app = builder.Build();
var forecast = app.NewVersionedApi();
var v1 = forecast.MapGroup("/weatherforecast").HasApiVersion(1.0);
v1.MapGet("/", (ApiVersion version) => Results.Ok());
This will not work out-of-the-box. There is no way to resolve ApiVersion
.
All of the current, supported strategies are insufficient for binding:
- The
ApiVersion
type cannot have aBindAsync
method- There is no direct dependency on ASP.NET Core; it’s just metadata
TryParse
:- Is delegated to
IApiVersionParser
so developers can change the parsing implementation - The value can come from multiple places and even simultaneously (ex: query string and header)
- ASP.NET Core will never understand this behavior
- Is delegated to
- The value is resolved through a feature which is not supported
- Defining and using a surrogate type in a lambda is confusing to developers that are accustomed to the behavior from
IModelBinder
- If a developer changes the parsing behavior, they would have to provide their own, alternate surrogate as well
There are 3 possible workarounds today:
- Use
HttpContext
as a parmaeter and retrieve the value viaHttpContext.GetRequestedApiVersion()
a. This isn’t very minimal - Use an explicit stand-in type; this a lot of tedious ceremony and is unnatural for consuming developers
- Use DI in an obtuse way
a.
services.AddTransient(sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.GetRequestedApiVersion()!
b. This is currently supported by explicitly opting intoservices.EnableApiVersionBinding()
Proposal
The proposal would be to add a service that can accept a surrogate type that can perform the binding. This would allow library authors to provide a similar experience to IModelBinder
without a developer having to do anything special. Surrogates would be registered through DI, which would enable anyone to add, remove, or replace surrogate types. Just as there is a 1-to-1 pairing of type to BindAsync
or IBindableFromHttpContext<T>
, so too, there can be exactly one corresponding surrogate type. A surrogate type will be considered as the last option before choosing IServiceProvider.GetService
.
The proposed API would be (final names TDB):
public interface IBindFromFromHttpContextService
{
bool TryGetSurrogate(Type targetType, [NotNullWhen(true)] out Type surrogateType);
}
public abstract class BindFromHttpContextSurrogate
{
protected BindFromHttpContextSurrogate(Type targetType) => TargetType = targetType;
public Type TargetType { get; }
}
The default implementation of the service would be:
internal sealed class DefaultBindFromFromHttpContextService : IBindFromFromHttpContextService
{
private readonly Dictionary<Type, Type> _surrogates;
public DefaultBindFromFromHttpContextService(
IEnumerable<BindFromHttpContextSurrogate> surrogates) =>
_surrogates => surrogates.ToDictionary(s => s.TargetType, s => s.GetType());
public bool TryGetSurrogate(Type targetType, [NotNullWhen(true)] out Type surrogateType) =>
_surrogates.TryGetValue(targetType, out surrogateType);
}
A surrogate type falls under the same rules for BindAsync
custom binding albeit on an alternate type.
An example implementation would be:
public sealed class ApiVersionBinder : BindFromHttpContextSurrogate
{
public ApiVersionBinder() : base(typeof(ApiVersion)) { }
public static ValueTask<ApiVersion> BindAsync(HttpContext context, ParameterInfo parameter)
{
var feature = context.Features.GetRequiredFeature<IApiVersioningFeature>();
return ValueTask.FromResult(feature.RequestedApiVersion!);
}
}
The surrogate can be registered in DI with:
services.TryAddEnumerable(ServiceDescriptor.Transient<BindFromHttpContextSurrogate, ApiVersionBinder>());
This is a generic approach can used to delegate any implementation of BindAsync
to an alternate type. Other frameworks, such as OData, could support Minimal APIs using this same mechanism.
Risks / unknowns
None that are immediately evident. The only thing changing in runtime behavior is which type to look for BindAsync
on.
Issue Analytics
- State:
- Created 9 months ago
- Comments:9 (5 by maintainers)
I’m gonna close this as a dupe
Let’s continue the discussion on that original issue with the DI based idea.