Corrupted form data when using an explicit index and the modelstate is not valid
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
### The Issue
If a razor page posts a collection of elements that use explicit indexing, where the explicit index is non sequential and non numerically ordered and the data they submit won't pass model validation on the server side. Then when the page is returned with the validation error the input fields will have incorrect data in them.
For example this data when posted (including the corresponding Parent.Child.Index fields) with a validation error will be rendered fine when the page returns.
- Parent.Children[0].Name
- Parent.Children[3].Name
- Parent.Children[1].Name
- Parent.Children[2].Name
As will this:
- Parent.Children[0].Name
- Parent.Children[1].Name
- Parent.Children[2].Name
- Parent.Children[10].Name
- Parnet.Children[11].Name
This however won't be:
- Parent.Children[0].Name
- Parent.Children[10].Name
- Parent.Children[1].Name
- Parent.children[2].Name
### To Reproduce
I have reproduced this by creating a simple model that has a parent class that contains a number of children. I render the parent out on a razor page and have a button that appends new children items dynamically in a random order using javascript. If the user then posts the data back with a non valid field (Note client side validation is turned off for this example.) then the form will show corrupted data.
If you debug the model that is posted back you can see that it is uncorrupted. It only gets corrupted when the page has been rendered out.
Code steps to reproduce the issue:
1. Create a brand new razor pages web app .netcore 3.1 application
2. Create a basic Model.
```
public class Parent
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public IList Children { get; set; }
}
public class Child
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public int ParentID { get; set; }
}
```
3. Create a new razorpage code-behind file to process the data:
```
public class ExplicitIndex : PageModel
{
[BindProperty]
public Parent Parent { get; set; }
public string JsonObject { get; set; }
public void OnGet()
{
Parent = new Parent { Id = 1, Name = "Parent" };
Parent.Children = new List { new Child { Id = 1, Name = "child1", ParentID=1 },
new Child { Id = 2, Name = "child2", ParentID=1 },
new Child { Id = 3, Name = "child3", ParentID=1 },
new Child { Id = 4, Name = "child4", ParentID=1 }};
}
public IActionResult OnPost()
{
if(!ModelState.IsValid)
{
JsonObject = JsonConvert.SerializeObject(Parent);
return Page();
}
return RedirectToPage("./Index");
}
}
```
4. Create the following razorpage file:
```
@page
@model WebApplication3.Pages.ExplicitIndex
@{
}
@Model.JsonObject
@for (int index = 0; index < Model.Parent.Children.Count; index++)
{
}
Name
ID
Parent ID
Add Row
@section Scripts{
@* Prevent client side form validation*@
@*@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}*@
function addItem() {
let table = document.getElementById("table");
let numRows = table.rows.length - 1;
let rowBefore = table.rows[getRandomInt(1, numRows)];
let addedRow = rowBefore.parentNode.insertBefore(document.createElement("tr"), rowBefore);
// Generate unique index value outside of existing index items
let rowNum = table.rows.length + 10;
addedRow.innerHTML = `
<td>
<input type="hidden" name="Parent.Children.Index" value="${rowNum}">
<input type="text" data-val="true" data-val-required="The Name field is required." id="Parent_Children_${rowNum}__Name" name="Parent.Children[${rowNum}].Name" value="">
<span class="text-danger field-validation-valid" data-valmsg-for="Parent.Children[${rowNum}].Name" data-valmsg-replace="true"></span>
</td>
<td>
<input type="number" data-val="true" data-val-required="The Id field is required." id="Parent_Children_${rowNum}__Id" name="Parent.Children[${rowNum}].Id" value="">
</td>
<td>
<input type="number" data-val="true" data-val-required="The ParentID field is required." id="Parent_Children_${rowNum}__ParentID" name="Parent.Children[${rowNum}].ParentID" value="">
</td>`
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
```
5. Start the application navigate to the razor page above, click the "Add Row" button and then click save (don't enter any info). The page will then be returned as it has failed validation. The page displays the JSON representation of the object and the form displays the returned form data. These don't match with the form data becoming corrupted.
### Further technical details
- ASP.NET Core version 3.1
- dotnet --info
- Visual Studio 2019
```
.NET SDK (reflecting any global.json):
Version: 5.0.100-preview.8.20417.9
Commit: fc62663a35
Runtime Environment:
OS Name: Windows
OS Version: 10.0.17763
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\5.0.100-preview.8.20417.9\
Host (useful for support):
Version: 5.0.0-preview.8.20407.11
Commit: bf456654f9
.NET SDKs installed:
2.2.207 [C:\Program Files\dotnet\sdk]
3.1.402 [C:\Program Files\dotnet\sdk]
5.0.100-preview.8.20417.9 [C:\Program Files\dotnet\sdk]
.NET runtimes installed:
Microsoft.AspNetCore.All 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.22 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.22 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 5.0.0-preview.8.20414.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.22 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 5.0.0-preview.8.20407.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 3.1.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 5.0.0-preview.8.20411.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
```
Contributor guide
Assessment
This issue has not been assessed yet.