Search code examples
c#ajaxasp.net-core.net-corerazor-pages

What is the correct syntax to bind a list of objects containing additional (nested) lists when posting forms


UPDATE: I've identified the root cause of my issue. It was failing due to the size of the form which I was posting. When I added in the nested list (ObjectC), it exceeded this length and failed to bind.

I resolved the issue by adding the following attribute to my PageModel:

[RequestFormLimits(ValueCountLimit = 5000)]
public class MyPageModel : PageModel

However, I still don't understand why it was just failing to bind, rather than throwing an exception. Looking in the logs I'm seeing a 400 response (which I hadn't noticed because I was originally throwing a NotImplementedException), but it was also entering the handler method and returning content (originally the aforementioned exception, and then Page() content when I changed it for testing the identified error).

If anyone can clarify the limits and nuances around posting a form in Razor Pages, or can link to some docs, it would be appreciated. My form is dynamic and could be quite large, so I'll need to understand these limitations and handle them gracefully. I did not explicitly set any form limits.

I'll post an update on my findings if I resolve/clarify this issue.


I'm attempting to bind a complex object which contains list of complex objects, which also contain their own list of complex objects; so nested lists of objects. The data binds to the page without error and I can see the pre-populated content. The issue occurs when attempting to post the form to the handler method.

I am able to successfully post and bind the parent object (ObjectA), all of it's data-type properties, as well as it's property which is a list of complex objects (ObjectB). However, I am unable to bind the list of objects (ObjectC) contained within ObjectB.

Because my code is lengthy and complex (as well as confidential), I'll provide a simple example of what I'm attempting.

My Classes and handler method:

// Classes
public class ObjectA
{
    public int Id { get; set; }
    public IList<ObjectB> ObjectBList { get; set; }
}

public class ObjectB
{
    public int Id { get; set; }
    public IList<ObjectC> ObjectCList { get; set; }
}

public class ObjectC
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// Handler method
public async Task<IActionResult> OnPostDoSomethingAsync(ObjectA objectA)
{
    // Do Something
}

My .cshtml pseudo-code; ClassA is the page model

I have various types of input fields, but I'm just using "hidden" for this example

<form id="myFormId">
    <input type="hidden" asp-for="@Model.Id" />
    @for (var i; i < @Model.ObjectBList.Count(); i++)
    {
        var objectCList = @Model.ObjectBList[i].ObjectCList.Where(z => z.Id < 4).OrderBy(z => z.Id).ToList(); // I need to perform some filtering logic in multiple places, so create a temp object
        <input type="hidden" asp-for="@Model.ObjectBList[i].Id" name="ObjectBList[@i].Id" />
        @for (var z; z < objectCList.Count(); z++)
        {
            <input type="hidden" asp-for="objectCList[z].Id" name="ObjectBList[@i].ObjectCList[@z].Id" />
    }
</form>

I would like to be able to submit this form both via a submit type button (using the standard Razor Page form submission/page reload) as well as via ajax. My current tests have been via an ajax post similar to the following:

$.post('/Page?handler=DoSomething', $(#myFormId).serialize(), function () { alert('success'); });

When I perform the post action with the code as-written above, I see content similar to the following in the payload:

Id: 1
ObjectB[0].Id: 1
ObjectB[0].ObjectC[0].Id: 1
ObjectB[0].ObjectC[1].Id: 2
ObjectB[1].Id: 2
ObjectB[1].ObjectC[0].Id: 1
ObjectB[1].ObjectC[1].Id: 2

The result of this payload is that my handler is hit but the parameter "objectA" is null (presumably because the content received did not match my expected object model and binding failed). My expectations is that I would have received a populated ObjectA, including a list of ObjectB, which would contain a list of ObjectC.

It's important to note that if I comment out my CSHTML code related to binding ObjectC, and just bind ObjectA and ObjectB, my payload looks as follows:

Id: 1
ObjectB[0].Id: 1
ObjectB[1].Id: 2

The result is that everything works as expected. My handler method is hit, and ObjectA is populated, including ObjectBList, which contains a list of ObjectB (as expected). So when I attempt to add in ObjectC, it breaks the binding and yields null in my handler.

I expect I am doing something wrong with how I am naming my input fields for ObjectC, and the resulting payload is incorrect. Any guidance on the correct syntax for such a scenario would be appreciated. If my approach is correct, then I presume I have a typo or similar issue that I'll need to find (none found thus far).


Solution

  • <input type="hidden" asp-for="objectCList[z].Id" name="ObjectBList[@i].ObjectCList[@z].Id" />

    You can remove the type="hidden" to see if the value set to it by asp-for="objectCList[z].Id", my test is not, so I have a little change like below, have a try like:

     @for (var i = 0; i < @Model.ObjectBList.Count(); i++)
     {
         var objectCList = Model.ObjectBList[@i].ObjectCList.Where(z => z.Id <4).Select(C => C.Id).ToList(); 
         <input type="hidden" asp-for="@Model.ObjectBList[i].Id" name="ObjectBList[@i].Id" />     
         @for (var z = 0; z <Model.ObjectCList.Count(); z++)
         {
             <input type="hidden" value="@objectCList[@z]" name="ObjectBList[@i].ObjectCList[@z].Id" />
         }
     }
    

    result:

    enter image description here

    Update

    A limit for the number of form entries to allow. Forms that exceed this limit will throw an InvalidDataException when parsed. Defaults to 1024.

    First way we can use RequestFormLimitsAttribute.ValueCountLimit to decorate the PageModel:

    [RequestFormLimits(ValueCountLimit = int.MaxValue)]
    public class MyPageModel : PageModel
    

    or set FormOptions.ValueCountLimit in Program.cs in .NET 6

    builder.Services.Configure<FormOptions>(x =>
    {
        x.ValueCountLimit = int.MaxValue;
    });
    

    Reference : FormOptions.ValueCountLimit Property and submit large form data with post action return 400 error in .NET 6