How do I bind List<object> using FromForm?

57 views Asked by At

I have an issue with model binding with ASP.NET Core controllers when trying to bind a list of DTO objects from form data. Using Swagger UI, I try to submit a list of DTO objects, but the list comes through empty.

Here's an example:

[Route("api/test")]
[ApiController]
public class TestController : ControllerBase
{
    [HttpPost]
    public ActionResult<List<Dto>> Post([FromForm] List<Dto> dtos)
    {
        // debug here, dtos is empty
        return Ok(dtos);
    }
}

and here's the DTO:

public class Dto
{
    public required string Field { get; set; }
}

I confirmed the issue lies with List<Dto>, because I had no problems with a list of primitives like public ActionResult<List<string>> Post([FromForm] List<string> strings){}.

Also, removing the [FromForm] so that Content-Type: multipart/form-data becomes Content-Type: application/json works, leading me to believe that the issue lies with how ASP.NET handles model binding.

Is there a limitation with binding lists of objects from form data in ASP.NET Core, or am I missing something in my approach/configuration (currently using default webapi scaffold)?

1

There are 1 answers

0
ArcherBird On BEST ANSWER

You are correct that the native binding for [FromForm] cannot handle complex objects within the form data. To accomplish this, you need to help the middleware understand how to bind to the Dto class. You can do this with a an IModelBinder like so:

public class DtoFormBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // Fetch the value of the argument by name and set it to the model state
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        
        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var objRepresentation = valueProviderResult.FirstValue;
        if (objRepresentation is null)
        {
            return Task.CompletedTask;
        }
        
        try
        {
            // Deserialize the provided value and set the binding result
            var result = JsonSerializer.Deserialize(
                objRepresentation,
                bindingContext.ModelType,
                new JsonSerializerOptions()
                {
                    PropertyNameCaseInsensitive = true
                });
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        catch(Exception e)
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

and decorate your Dto class with this binder like this:

[ModelBinder<DtoFormBinder>]
public class Dto
{
    public required string Field { get; set; }
}

Now you should be able to bind to the models on the form.