question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Support BindAsync Surrogates in Minimal APIs

See original GitHub issue

Summary

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 a BindAsync 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
  • 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:

  1. Use HttpContext as a parmaeter and retrieve the value via HttpContext.GetRequestedApiVersion() a. This isn’t very minimal
  2. Use an explicit stand-in type; this a lot of tedious ceremony and is unnatural for consuming developers
  3. Use DI in an obtuse way a. services.AddTransient(sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.GetRequestedApiVersion()! b. This is currently supported by explicitly opting into services.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:closed
  • Created 9 months ago
  • Comments:9 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
davidfowlcommented, Dec 10, 2022

I’m gonna close this as a dupe

0reactions
davidfowlcommented, Dec 10, 2022

Let’s continue the discussion on that original issue with the DI based idea.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Parameter binding in Minimal API applications
Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers.
Read more >
Exploring the model-binding logic of minimal APIs
In this post I look at how the RequestDelegateFactory.CreateArgument() chooses which source to use for model-binding and the logic involved.
Read more >
Custom Model Binding in ASP.NET 6.0 Minimal APIs
NET 6.0 Minimal APIs to customize model binding. ... BindAsync is a static method that can be added to your types in order...
Read more >
How to use parameter binding in minimal APIs in ASP.NET ...
To use parameter binding in minimal APIs in ASP.NET Core 7, developers need to define action methods that accept parameters. For example, the ......
Read more >
Minimal APIs in .NET 6: How to bind [FromQuery]
1. For route, query, and header binding sources, bind custom types by adding a static TryParse method for the type. 2. Control the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found