Lo scenario che analizzeremo in questo articolo è quello dell’invio di file di grandi dimensioni tramite richiesta HTTP nel contesto, più specifico, di un’applicazione realizzata con Blazor WebAssembly che effettua il caricamento di file su server tramite API REST.
HttpClient fornisce, in .NET, i metodi per inviare e ricevere richieste HTTP. Esso fa parte dei servizi predefiniti di Blazor WebAssembly ed è lo strumento principe per l’accesso ai dati ed ogni altra forma di interazione con il backend, compreso lo scambio di file.
In questo contesto, tramite una specifica implementazione di HttpMessageHandler (BrowserHttpHandler), sfrutta il browser per la gestione del traffico HTTP in background.
Il problema
Nel caso di un file di grandi dimensioni è importante prestare particolare attenzione al modo in cui la richiesta viene creata. Esso può essere trasmesso sotto forma di stream, tramite un’istanza della classe StreamContent, evitando così di doverlo manipolare nella sua interezza.
private static async Task FileUpload(IBrowserFile file)
{
await using var stream = file.OpenReadStream(long.MaxValue);
using var request = new HttpRequestMessage(HttpMethod.Post, "file");
using var content = new MultipartFormDataContent
{
{ new StreamContent(stream), "file", "test_file.txt" }
};
request.Content = content;
await client.SendAsync(request);
}
Ciò di cui ci si rende conto rapidamente è che, nonostante questi accorgimenti, si riscontrano gravi malfunzionamenti legati ad un eccessivo utilizzo della memoria in fase di invio.

Indagando i sorgenti di BrowserHttpHandler non è difficile individuarne la causa. Il metodo SendAsync, che gestisce l’inoltro delle richieste, crea una copia in memoria di request.Content prima dell’invio, alloncandone l’intera dimensione.
if (request.Content != null)
{
if (request.Content is StringContent)
{
requestObject.SetObjectProperty(
"body",
await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: true)
);
}
else
{
using (Uint8Array uint8Buffer = Uint8Array.From(
await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: true))
)
{
requestObject.SetObjectProperty("body", uint8Buffer);
}
}
}
Possibili soluzioni
Nasce da qui la necessità di trovare vie alternative che permettano di aggirare il problema, senza imporre limitazioni a priori. Analizzeremo di seguito due tra le possibili soluzioni, facendo una panoramica del codice condiviso al seguente link:
https://github.com/DiegoZunino/Blazor.LargeFileUpload
Invio di file in chunks
La prima soluzione consiste nell’inviare il file suddiviso in parti (chunks) effettuando richieste multiple tramite HttpClient.
Frontend
Il metodo UploadInChunks si occupa di processare il file, suddividerlo ed inviarne le parti, una ad una.
public async Task UploadInChunks(IBrowserFile file)
{
var first = true;
long offset = 0;
var numChunks = file.Size / _chunkSize;
var remainder = file.Size % _chunkSize;
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.Name).ToLowerInvariant()}";
await using var inStream = file.OpenReadStream(long.MaxValue);
for (var i = 0; i < numChunks; i++)
{
var buffer = new byte[_chunkSize];
await inStream.ReadAsync(buffer, 0, buffer.Length);
var chunk = new FileChunk
{
Data = buffer,
FileName = fileName,
Offset = offset += _chunkSize,
First = first
};
await _apiClient.ChunkUpload(chunk);
first = false;
}
if (remainder > 0)
{
var buffer = new byte[remainder];
await inStream.ReadAsync(buffer, 0, buffer.Length);
var chunk = new FileChunk
{
Data = buffer,
FileName = fileName,
Offset = offset,
First = first
};
await _apiClient.ChunkUpload(chunk);
}
}
Il file viene letto tramite l’apertura di uno stream, da cui ciclicamente vengono estratte porzioni successive ed equivalenti di byte (_chunkSize) che vengono incapsulate in oggetti di tipo FileChunk, con i relativi metadati ed inviate tramite HttpClient.
Backend
La action ChunkUpload espone l’endpoint che riceve le singole parti. Esso si occupa di ricostruire progressivamente il file e salvarlo su disco.
[HttpPost("chunk")]
public async Task<IActionResult> ChunkUpload([FromBody] FileChunk request)
{
var filePath = Path.Combine(_dirName, request.FileName);
if (request.First && System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
}
await using var stream = System.IO.File.OpenWrite(filePath);
stream.Seek(request.Offset, SeekOrigin.Begin);
stream.Write(request.Data, 0, request.Data.Length);
return Ok();
}
Questo approccio permette di risolvere i problemi di allocazione della memoria pur contiuando ad utilizzare HttpClient. La molteplicità di chiamate però, sopratutto nel caso di file molto grandi, rappresenta un overhead non trascurabile.
Stream via JavaScript
La seconda soluzione consiste nell’effettuare lo streaming del file incapsulato in un oggetto FormData e sfruttare l’interoperabilità con JavaScript per inoltrarlo direttamente tramite Fetch API.
Frontend
La classe FileUploadJsInterop agisce da intermediario tra .NET e JavaScript assumendosi la reponsabilità di gestire l’interoperabilità tra i due.
public class FileUploadJsInterop : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> _moduleTask;
public FileUploadJsInterop(IJSRuntime jsRuntime)
{
var importPath = "./js/fileUploadJsInterop.js";
_moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>("import", importPath).AsTask());
}
public async ValueTask<FileStreamResponse> FileStream(string fileInputElementId)
{
var module = await _moduleTask.Value;
return await module.InvokeAsync<FileStreamResponse>("fileStream", fileInputElementId);
}
public async ValueTask DisposeAsync()
{
if (_moduleTask.IsValueCreated)
{
var module = await _moduleTask.Value;
await module.DisposeAsync();
}
}
}
Il metodo FileStream offre l’accesso all’omonima funzione JavaScript.
'use strict';
export async function fileStream (fileInputElementId) {
let fileInputElement = document.getElementById(fileInputElementId);
let data = new FormData();
data.append('File', fileInputElement.files[0])
return { statusCode : await AJAXSubmit('/fileupload/stream', data).then(response => response.status) };
}
export async function AJAXSubmit (url, data) {
return await fetch(url, {
method: 'POST',
body: data
});
}
La funzione fileStream si occupa di creare l’oggetto FormData e di inoltrarlo al backend nel body della richiesta HTTP tramite metodo fetch.
Backend
La action FileStream espone l’endpoint che prende in carico tale richiesta. Il file viene letto tramite l’apertura di uno stream e copiato progressivamente su disco.
[HttpPost("stream")]
[RequestSizeLimit(long.MaxValue)]
[DisableFormValueModelBinding]
public async Task<IActionResult> FileStream()
{
if (MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
try
{
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType));
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
var contentDisposition = MultipartRequestHelper.GetContentDisposition(section);
if (MultipartRequestHelper.HasAllowedFileContentDisposition(contentDisposition))
{
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(contentDisposition.FileName.Value).ToLowerInvariant()}";
await using var targetStream = System.IO.File.Create(Path.Combine(_dirName, fileName));
await section.Body.CopyToAsync(targetStream);
return Ok();
}
}
catch (InvalidDataException ex)
{
return BadRequest();
}
}
return BadRequest();
}
Il filtro [DisableFormValueModelBinding] disabilita il model binding, evitando che il file venga salvato in memoria. La richiesta viene quindi gestita e validata manualmente. Invece l’attributo [RequestSizeLimit] estende il limite massimo di dimensione della richiesta.
Conclusioni
Le alternative proposte in questo articolo sono solo due tra le possibili soluzioni al problema, senza alcuna pretesa di essere le più corrette. Le pubblico con la speranza che possano essere di aiuto a qualcuno nell’attesa di una, speriamo rapida, soluzione “ufficiale” al problema.
Come già detto trovate il codice qui: https://github.com/DiegoZunino/Blazor.LargeFileUpload.
Alla prossima!