I am using new FormData() and formData.append(data[${index}].supportingDocumentation, supportingDocs[i]); to implement this feature, it works fine.
Here is my sample code
Index.cshtml
@{
ViewData["Title"] = "Home Page";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Historical Preprint Review Upload</title>
</head>
<body>
<h1>Upload Historical Preprint Review</h1>
<form id="uploadForm" enctype="multipart/form-data">
<div id="roundsContainer">
<div class="round">
<h3>Round 1</h3>
<label>PPID: <input type="text" name="ppid"></label><br>
<label>Round: <input type="number" name="round"></label><br>
<label>Preprint: <input type="file" name="preprint"></label><br>
<label>Supporting Documentation: <input type="file" name="supportingDocumentation" multiple></label><br>
</div>
</div>
<button type="button" onclick="addRound()">Add Another Round</button><br><br>
<button type="button" onclick="submitForm()">Submit</button>
</form>
<script>
let roundIndex = 1;
function addRound() {
roundIndex++;
const roundsContainer = document.getElementById('roundsContainer');
const newRound = document.createElement('div');
newRound.classList.add('round');
newRound.innerHTML = `
<h3>Round ${roundIndex}</h3>
<label>PPID: <input type="text" name="ppid"></label><br>
<label>Round: <input type="number" name="round"></label><br>
<label>Preprint: <input type="file" name="preprint"></label><br>
<label>Supporting Documentation: <input type="file" name="supportingDocumentation" multiple></label><br>
`;
roundsContainer.appendChild(newRound);
}
async function submitForm() {
const form = document.getElementById('uploadForm');
const formData = new FormData();
const rounds = form.querySelectorAll('.round');
rounds.forEach((round, index) => {
formData.append(`data[${index}].ppid`, round.querySelector('input[name="ppid"]').value);
formData.append(`data[${index}].round`, round.querySelector('input[name="round"]').value);
formData.append(`data[${index}].preprint`, round.querySelector('input[name="preprint"]').files[0]);
const supportingDocs = round.querySelector('input[name="supportingDocumentation"]').files;
for (let i = 0; i < supportingDocs.length; i++) {
formData.append(`data[${index}].supportingDocumentation`, supportingDocs[i]);
}
});
try {
const response = await fetch('/Home/UploadHistoricalReview', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log(result);
alert('Upload successful!');
} catch (error) {
console.error('Error:', error);
alert('Upload failed!');
}
}
</script>
</body>
</html>
UploadHistoricalReview method
[HttpPost]
public async Task<ActionResult> UploadHistoricalReview([FromForm] List<HistoricalPreprintReview> data)
{
return Ok(new { message = "Upload successful!" });
}
HistoricalPreprintReview.cs
public class HistoricalPreprintReview
{
[ModelBinder(Name = "ppid")]
public int ppid { get; set; }
[ModelBinder(Name = "round")]
public int round { get; set; }
[ModelBinder(Name = "preprint")]
public IFormFile preprint { get; set; }
[ModelBinder(Name = "supportingDocumentation")]
public List<IFormFile> supportingDocumentation { get; set; }
}
Test Result


Of course, we can also implement this by customizing ModelBinder.
HistoricalPreprintReviewModelBinder.cs
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MVCDemo.Models;
namespace MVCDemo
{
public class HistoricalPreprintReviewModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var form = bindingContext.HttpContext.Request.Form;
var result = new List<HistoricalPreprintReview>();
int index = 0;
while (form.ContainsKey($"data[{index}].ppid"))
{
var review = new HistoricalPreprintReview
{
ppid = int.Parse(form[$"data[{index}].ppid"]),
round = int.Parse(form[$"data[{index}].round"]),
preprint = form.Files.GetFile($"data[{index}].preprint")
};
var supportingDocumentation = form.Files
.Where(f => f.Name.StartsWith($"data[{index}].supportingDocumentation"))
.ToList();
review.supportingDocumentation = supportingDocumentation;
result.Add(review);
index++;
}
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
}
HistoricalPreprintReview.cs
[ModelBinder(BinderType = typeof(HistoricalPreprintReviewModelBinder))]
public class HistoricalPreprintReview
{
public int ppid { get; set; }
public int round { get; set; }
public IFormFile preprint { get; set; }
public List<IFormFile> supportingDocumentation { get; set; }
}