Handle MultipleUpload file uploads with ASP.NET Core minimal APIs using
IFormFile.
This guide covers both standard multipart uploads and chunked uploads.
Use ASP.NET Core minimal APIs to receive multipart/form-data uploads via IFormFile. Files are saved to disk with a unique GUID filename.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.WithHeaders("Content-Type", "X-Upload-Id", "X-Chunk-Index",
"X-Chunk-Count", "X-File-Name", "X-File-Size");
});
});
var app = builder.Build();
app.UseCors();
var uploadDir = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
var chunkDir = Path.Combine(uploadDir, "chunks");
Directory.CreateDirectory(uploadDir);
app.MapPost("/api/upload", async (IFormFile file) =>
{
var fileGuid = Guid.NewGuid().ToString();
var ext = Path.GetExtension(file.FileName);
var destPath = Path.Combine(uploadDir, fileGuid + ext);
using (var stream = new FileStream(destPath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Results.Ok(new
{
success = true,
fileGuid,
fileName = file.FileName,
fileSize = file.Length
});
})
.DisableAntiforgery();
Each chunk arrives as a raw body with custom headers. Store chunks in a temporary directory and assemble the final file once all chunks have arrived.
// Add after the /api/upload endpoint
app.MapPost("/api/upload-chunk", async (HttpContext context) =>
{
var headers = context.Request.Headers;
var uploadId = headers["X-Upload-Id"].ToString();
var chunkIndex = int.Parse(headers["X-Chunk-Index"].ToString());
var chunkCount = int.Parse(headers["X-Chunk-Count"].ToString());
var fileName = headers["X-File-Name"].ToString();
var fileSize = long.Parse(headers["X-File-Size"].ToString());
if (string.IsNullOrEmpty(uploadId) || chunkCount == 0)
{
return Results.BadRequest(new { success = false, error = "Missing headers" });
}
var sessionDir = Path.Combine(chunkDir, uploadId);
Directory.CreateDirectory(sessionDir);
// Save this chunk from raw body
var chunkPath = Path.Combine(sessionDir, $"chunk_{chunkIndex}");
using (var fs = new FileStream(chunkPath, FileMode.Create))
{
await context.Request.Body.CopyToAsync(fs);
}
// Count received chunks
var received = Directory.GetFiles(sessionDir, "chunk_*").Length;
if (received == chunkCount)
{
// Assemble final file
var fileGuid = Guid.NewGuid().ToString();
var ext = Path.GetExtension(fileName);
var finalPath = Path.Combine(uploadDir, fileGuid + ext);
using (var output = new FileStream(finalPath, FileMode.Create))
{
for (var i = 0; i < chunkCount; i++)
{
var cp = Path.Combine(sessionDir, $"chunk_{i}");
using var chunkStream = File.OpenRead(cp);
await chunkStream.CopyToAsync(output);
}
}
// Clean up chunks
Directory.Delete(sessionDir, recursive: true);
return Results.Ok(new
{
success = true,
fileGuid,
fileName,
fileSize
});
}
return Results.Ok(new { success = true, chunksReceived = received });
});
app.Run();
Increase the maximum request body size in Program.cs
or appsettings.json if you need to accept files larger than 30 MB.
// In Program.cs, before builder.Build()
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 200 * 1024 * 1024; // 200 MB
});
Both endpoints must return this JSON structure on successful upload completion.
{
"success": true,
"fileGuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"fileName": "photo.jpg",
"fileSize": 204800
}
Initialize MultipleUpload on the client, pointing to your ASP.NET Core API endpoints.
<link rel="stylesheet" href="multipleupload.css" />
<div id="uploader"></div>
<script src="multipleupload.js"></script>
<script>
var uploader = new MultipleUpload("#uploader", {
uploadUrl: "/api/upload",
chunkUploadUrl: "/api/upload-chunk",
chunkSize: 2 * 1024 * 1024, // 2 MB chunks
onFileUploaded: function (file, response) {
console.log("Uploaded:", response.fileName, response.fileGuid);
}
});
</script>