I have started to take advantage of Axios header option multipart/form-data so I can go utilize Typescript objects for my requests to bind to my ASP.NET Core models, however List<IFormFile> properties do not seem to bind and stay as null, while my other properties bind fine, including a single IFormFile property.
My Typescript interface is as follows:
export interface AddReview {
primaryDoc: File; // Required, PDF
supportingDocuments: File[] | null;
stateId: number; // Required
}
And my axios request is:
const addReview = async (submissionData : SDP.AddReview) : Promise<any | null> => {
const data = api.post(`/sdp/Review/`, submissionData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => {
const apiResponse : API.Response = response.data
const review : any = apiResponse.data
return review
}).catch(error => {
if(error.response.status == 400) {
router.push({ name: 'Error'})
}
return null
})
return data
}
Then this should bind to my AddReviewDTO model class in C#:
public class AddReviewDTO
{
[Required]
public required IFormFile PrimaryDoc { get; set; }
public List<IFormFile>? SupportingDocuments { get; set; }
[Required]
public required int StateId { get; set; }
}
The controller action that is trying to achieve this action is as follows:
public async Task<IActionResult> AddReviewAsync([FromForm] AddReviewDTO addDTO)
{
string json = JsonSerializer.Serialize(addReviewDTO);
}
I know there are instances where custom model binders are required in ASP.NET Core but this seems so basic so I am hoping this can be done without that sort of solution as I want to reuse this approach on many scenarios with different models. In addition, I do not want to go back to the approaching of looping through my entire object to add to FormData manually as this was tedious.
Here is a similar question but no answer without switching to complete FormData approach which I use to use and want to get away from:
.Net 7 API won't accept List<IFormFile>
My only solution so far is this utility function I had ChatGPT Create:
const objectToFormData = (
obj: Record<string, any>,
form: FormData = new FormData(),
namespace: string = ''
): FormData => {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (value === undefined || value === null) return;
const formKey = namespace ? `${namespace}.${key}` : key;
if (value instanceof Date) {
form.append(formKey, value.toISOString());
} else if (value instanceof File || value instanceof Blob) {
form.append(formKey, value, value.name);
} else if (Array.isArray(value)) {
value.forEach((element) => {
if (element instanceof File || element instanceof Blob) {
// ✅ Multiple files should use the same key (no index)
form.append(formKey, element, element.name);
} else if (typeof element === 'object' && element !== null) {
// Use dot notation or any custom naming here
objectToFormData(element, form, formKey);
} else {
form.append(formKey, String(element));
}
});
} else if (typeof value === 'object') {
objectToFormData(value, form, formKey);
} else {
form.append(formKey, String(value));
}
});
return form;
}
This will convert the object to FormData.
indexesoption totrue, ie, just afterheaders...formSerializer: { indexes: true }. See github.com/axios/….nullmay be another viable option, I can't remember how .Net expects to receive repeated form-data fieldsList<IFormFile>withList<FormFile>? I'm curious - how would the serializer know which concrete type to instantiate?