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.
ASP.NET expects repeated fields in multipart/form-data
payloads to have no []
or [n]
suffixes whereas Axios by default opts for the PHP-style []
suffix.
You can disable the index suffixes by configuring the formSerializer
option, best applied at your instance creation...
const api = axios.create({
// ...
formSerializer: {
indexes: null,
}
})
or inline with your request
const data = api.post(`/sdp/Review/`, submissionData, {
headers: {
'Content-Type': 'multipart/form-data'
},
formSerializer: {
index: null,
},
});
See Automatic serialization to FormData
indexes: null|false|true = false
- controls how indexes will be added to unwrapped keys offlat
array-like objects.
null
- don't add brackets (arr: 1
,arr: 2
,arr: 3
)false
(default) - add empty brackets (arr[]: 1
,arr[]: 2
,arr[]: 3
)true
- add brackets with indexes (arr[0]: 1
,arr[1]: 2
,arr[2]: 3
)