The ASP.NET team built ASP.NET Core Minimal APIs to help developers build more concise and efficient web APIs. The approach has fewer features in favor of improved resource utilization and higher request throughput. One of the features to be “removed” is ASP.NET Core MVCs robust model binding. Typically, requests coming into a Minimal API endpoint are expected to be JSON. If you’re a fan of the Minimal API approach, there are ways to enable manual model binding, but it can be very tedious.

In this post, we’ll see how we can add back in some of ASP.NET Core MVC’s model binding to take advantage of it within our Minimal API apps.

Incoming Form-Based Requests

The content type of application/x-www-form-urlencoded rules the internet, and you can commonly use it to send information from an HTML client to the server. But, first, let’s look at a simple POST request with some information.

POST https://localhost:7113/hi
Content-Type: application/x-www-form-urlencoded

Name=Khalid&Age=38

We have a minimal endpoint on the back-end that expects a Person parameter type.

public record Person(string Name, int Age);

Luckily, with Minimal APIs, types can define a BindAsync method to opt in to model binding.

public record Person(string Name, int Age)
{
    public static async ValueTask<Person?> BindAsync(HttpContext httpContext, ParameterInfo parameter)
    {
        // return type
    }
}

Like I mentioned in the introduction, mapping from the HttpContext can be tedious. What if we could use some of the mechanisms already found within ASP.NET Core MVC in our Minimal API apps?

Registering ASP.NET Core MVC Core with Minimal APIs

Well, luckily, we can register elements of ASP.NET Core MVC without using the relatively slower request pipeline. First, let’s take a look at our application.

using System.Reflection;
using FormBinding;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMvcCore();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/hi", (Person person) =>
{
    return Results.Ok(person);
});

app.Run();

public record Person(string Name, int Age)
{
    public static async ValueTask<Person?> BindAsync(HttpContext httpContext, ParameterInfo parameter)
    {
        return await httpContext.BindFromForm<Person>();
    }
}

We can see that we have a /hi endpoint that expects a Person parameter. We also see our Person type with the BindAsync method ready to generate an instance from the request. Most importantly, at the top of our application, we make a registration call to AddMvcCore, which adds model binding services required for the BindFromForm method to work. Let’s see how that works.

using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace FormBinding;

public static class BindingExtensions
{
    public static async Task<T?> BindFromForm<T>(this HttpContext httpContext)
    {
        var serviceProvider = httpContext.RequestServices;
        var factory = serviceProvider.GetRequiredService<IModelBinderFactory>();
        var metadataProvider = serviceProvider.GetRequiredService<IModelMetadataProvider>();

        var metadata = metadataProvider.GetMetadataForType(typeof(T));
        var modelBinder = factory.CreateBinder(new() {
            Metadata = metadata
        });

        var context = new DefaultModelBindingContext
        {
            ModelMetadata = metadata,
            ModelName = string.Empty,
            ValueProvider = new FormValueProvider(
                BindingSource.Form,
                httpContext.Request.Form,
                CultureInfo.InvariantCulture
            ),
            ActionContext = new ActionContext(
                httpContext, 
                new RouteData(), 
                new ActionDescriptor()),
            ModelState = new ModelStateDictionary()
        };
        await modelBinder.BindModelAsync(context);
        return (T?) context.Result.Model;
    }
}

The extension method finds the IModelBinderFactory and IModelMetadataProvider services in our services collection instance. These types ultimately allow us to create a ComplexObjectModelBinder which we set up to process information from our FormValueProvider. Finally, to make our endpoint parameter, we can call BindModelAsync with our DefaultModelBindingContext.

If everything has gone according to plan, we should see our endpoint return a JSON representation of our Person type, hydrated initially from the IFormCollection of the initial request.

https://localhost:7113/hi

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 10 Mar 2022 21:07:24 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "name": "Khalid",
  "age": 38
}

Hooray!

Conclusion

The nice thing about this approach is it uses many of the features found in ASP.NET Core MVC. In addition, the utilization of existing MVC functionality means we can use many of the other value providers, including FormFileValueProvider, JQueryFormValueProvider, QueryStringValueProvider, RouteValueProvider, and so many more. I hope you found this blog post helpful, and if you have thoughts you’d like to share with me, please do so on Twitter at @buhakmeh. As always, thank you for reading.