My code is working, but I'm looking to see if I'm doing this in the 'proper' fashion. I have a Minimal API that supports file uploads but also I needed to pass required meta data along with the files, and since [FromForm]
isn't supported yet, I did custom model binding via BindAsync
method.
Original code:
// Program.cs
builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>( options => {
// MultipartBodyLengthLimit is in kilobytes (KB) 10 * 1024
options.MultipartBodyLengthLimit = 1024 * 1; // Testing limit, will be bigger in production
});
var app = builder.Build();
app.MapPost( "/api/document-center/upload", DocumentCenterUploadAsync );
// Endpoint implementation...
async Task<IResult> DocumentCenterUploadAsync( ApiPayload apiPayload )
{
var postedFile = appApiPayload.PostedFiles.FirstOrDefault();
var category = apiPayload.Category;
// Upload document
}
// ApiPayload Class
public class ApiPayload
{
public required string Category { get; init; }
public required IFormFileCollection PostedFiles { get; init; } = new FormFileCollection();
public static async ValueTask<ApiPayload?> BindAsync(HttpContext context)
{
var form = await context.Request.ReadFormAsync();
return new ApiPayload
{
Category = form[ "Category" ],
PostedFiles = form.Files
};
}
}
This worked well when the limit isn't exceeded. But when file is larger than 1MB, var form = await context.Request.ReadFormAsync();
throws an InvalidDataException
. But I need put a custom message and 'input id' based on which endpoint is using the ApiPayload
parameter.
Below was my first attempt using AddEndpointFilter
with the following issues:
- The filter never executes before custom model binding occurs, so I can't catch exception.
- Unless I'm missing something, the 'message' check for length exceeded is only way I could figure out to tell if this type of exception occurred (since I think
InvalidDataException
is used for multiple scenarios).
app.MapPost( "/api/document-center/upload", DocumentCenterUploadAsync )
.AddEndpointFilter(async (efiContext, next) =>
{
try
{
return await next(efiContext);
}
catch ( InvalidDataException ex ) when ( ex.Message.IndexOf( "Multipart body length limit" ) > -1 )
{
return Results.Extensions.BadResponse(
new Dictionary<string, string>{
{ "iUpload", "You must select a document with a size less than 5MB to upload." }
}
);
}
});
So unfortunately, I had to put the try catch
inside my ApiPayload
class and communicate back to the endpoint. Below is what I came up with, with the following concerns:
- Try/catch is in a model class and have to 'communicate' to whatever endpoint might be using it.
- 'Message' check issue still.
- Having a generic
IEndpointFilter
that looked for input id and message info viaWithMetadata
andefiContext.HttpContext.GetEndpoint().Metadata
seemed like clean solution compared to what I have (lot of ceremony to pull it off). - Had to make
ApiPayload?
nullable since myBindAsync
could return null now. Also having to dereference via!
on first call toapiPayload
.
// Changed ApiPayload.BindAsync to have try/catch
public static async ValueTask<ApiPayload?> BindAsync(HttpContext context)
{
try
{
var form = await context.Request.ReadFormAsync();
return new ApiPayload
{
Category = form[ "Category" ],
PostedFiles = form.Files
};
}
catch ( InvalidDataException ex ) when ( ex.Message.IndexOf( "Multipart body length limit" ) > -1 )
{
context.Items[ "MaxRequestLengthException" ] = ex;
return null;
}
}
// Updated filter to look for context.Items
app.MapPost( "/api/document-center/upload", DocumentCenterUploadAsync )
.AddEndpointFilter(async (efiContext, next) =>
{
var ex = efiContext.HttpContext.Items[ "MaxRequestLengthException" ];
if ( ex != null )
{
return Results.Extensions.BadResponse( new Dictionary<string, string>{ { "iUpload", "You must select a document with a size less than 5MB to upload." } } );
}
return await next(efiContext);
});
// Had to update the signature of DocumentCenterUploadAsync to allow nullable and dereference on first usage
async Task<IResult> DocumentCenterUploadAsync( ApiPayload? apiPayload )
{
var postedFile = appApiPayload!.PostedFiles.FirstOrDefault();
var category = apiPayload.Category;
// Upload document
}
Is this my best option to achieve my goal?