You can write a custom binder. This is for NET 9, but it should work for NET Core in general (I think).
Here's a sample of one I wrote for DataTables.Net, which submits GETs in a similar way...
This are my classes
public class DataTableRequest : IDataTableRequest
{
public int? Draw { get; set; }
public IEnumerable<Column>? Columns { get; set; }
public IEnumerable<Order>? Order { get; set; }
public int? Start { get; set; }
public int? Length { get; set; }
public ISearch? Search { get; set; }
public Dictionary<string, string?>? OtherValues { get; set; }
}
public class Column : IColumn
{
public string? Data { get; set; }
public string? Name { get; set; }
public bool? Searchable { get; set; }
public bool? Orderable { get; set; }
public Search? Search { get; set; }
}
public class Search : ISearch
{
public string? Value { get; set; }
public bool? Regex { get; set; }
}
public class Order : IOrder
{
public int? Column { get; set; }
public string? Dir { get; set; }
public string? Name { get; set; }
}
And these are my binders
// DataTableRequestBinder.cs
using System.Text.RegularExpressions;
using CorreoNet.Web.Models.DataTablesNet;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace CorreoNet.Web.Binders;
public class DataTableRequestBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await Task.Delay(0);
var request = bindingContext.HttpContext.Request;
var values = bindingContext.ValueProvider;
// initialize with the simple types
var model = new DataTableRequest
{
Draw = int.TryParse(values.GetValue("draw").FirstValue, out var draw) ? draw : null,
Start = int.TryParse(values.GetValue("start").FirstValue, out var start) ? start : null,
Length = int.TryParse(values.GetValue("length").FirstValue, out var length) ? length : null,
Columns = new List<Column>(),
OtherValues = new Dictionary<string, string?>(),
};
// get columns
if (values.ContainsPrefix("columns"))
{
var keys = request.Query.Keys.Where(k => k.StartsWith("columns[")).ToList();
var colNumbers = keys.Select(k => Regex.Match(k, @"columns\[(\d+)\]").Groups[1].Value).Distinct().ToList();
var columns = new List<Column>();
foreach (var colNumber in colNumbers)
{
var column = new Column
{
Data = values.GetValue($"columns[{colNumber}][data]").FirstValue,
Name = values.GetValue($"columns[{colNumber}][name]").FirstValue,
Searchable = bool.TryParse(values.GetValue($"columns[{colNumber}][searchable]").FirstValue, out var searchable) ? searchable : null,
Orderable = bool.TryParse(values.GetValue($"columns[{colNumber}][orderable]").FirstValue, out var orderable) ? orderable : null,
Search = new Search
{
Value = values.GetValue($"columns[{colNumber}][search][value]").FirstValue,
Regex = bool.TryParse(values.GetValue($"columns[{colNumber}][search][regex]").FirstValue, out var searchRegex) ? searchRegex : null,
}
};
columns.Add(column);
}
model.Columns = columns;
}
// get order
if (values.ContainsPrefix("order"))
{
var keys = request.Query.Keys.Where(k => k.StartsWith("order[")).ToList();
var colNumbers = keys.Select(k => Regex.Match(k, @"order\[(\d+)\]").Groups[1].Value).Distinct().ToList();
var orders = new List<Order>();
foreach (var colNumber in colNumbers)
{
var order = new Order
{
Column = int.TryParse(values.GetValue($"order[{colNumber}][column]").FirstValue, out var column) ? column : null,
Dir = values.GetValue($"order[{colNumber}][dir]").FirstValue,
Name = values.GetValue($"order[{colNumber}][name]").FirstValue,
};
orders.Add(order);
}
model.Order = orders;
}
// get search
if (values.ContainsPrefix("search"))
{
model.Search = new Search()
{
Value = values.GetValue("search[value]").FirstValue,
Regex = bool.TryParse(values.GetValue("search[regex]").FirstValue, out var searchRegex) ? searchRegex : null,
};
}
// get other keys
var otherKeys = request.Query.Keys.Where(k =>
!k.StartsWith("columns[") &&
!k.StartsWith("draw") &&
!k.StartsWith("order[") &&
!k.StartsWith("search[") &&
!k.StartsWith("start") &&
!k.StartsWith("length")
).ToList();
foreach (var key in otherKeys)
model.OtherValues.Add(key, values.GetValue(key).FirstValue);
// bind!!!
bindingContext.Result = ModelBindingResult.Success(model);
}
}
and the provider, which you can add in your program.cs
// DataTableRequestBinderProvider.cs
using CorreoNet.Web.Models.DataTablesNet;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
namespace CorreoNet.Web.Binders;
public class DataTableRequestBinderProvider: IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
ArgumentNullException.ThrowIfNull(context);
return (context.Metadata.ModelType == typeof(DataTableRequest)) ?
new BinderTypeModelBinder(typeof(DataTableRequestBinder)) :
null;
}
}
I know it isn't exactly what you're looking for, but it did certainly helped me fix the same problem you have. Maybe it helps you a little bit.
Part[] PartstoICollection<Part> Parts? And can you show the class definition forPart?Parthave a parameterless constructor?Get?