Upload di un file e un form su una action ASP.NET Web Api con MultiPartFormDataContent

Lavorando con ASP.NET Web Api vi sarà sicuramente capitato di dover caricare lato server un file presente sul computer dell'utente. Se state usando un client basato su JavaScript - magari jQuery, se non volete farvi del male con XHR - è certamente una cosa piuttosto semplice: sono decide, per non dire centinaia, i controlli che si trovano in rete che già fanno quello che vi serve; basta trovare quello giusto che risponde alle vostre esigenze, aggiungere il riferimeto allo script, e....via!

Se poi, come sovente accade, dovete passare anche dei dati "form" nella richiesta di upload - magari per associare il file caricato sul server ad una entità presente nella vostra base dati - l'operazione richiede qualche sforzo in più, pur rimanendo fattibile e molto documentata. Parlo sempre di JavaScript, eh...

Ma se dovete fare questa cosa da .NET, magari un'applicazione WPF o una semplice console application, anche eseguire un semplice upload non è banalissimo. I post che si trovano in rete sono talvolta discordanti, e tendono a confondere il lettore...sopratutto se il lettore sono io ;) .

Detto questo, dopo numerosi tentativi e un paio di orette perse, sono riuscito a trovare il "bandolo della matassa". Ma non perdiamoci in chiacchere e vediamo subito un po' di codice. Questa è la action Web API che ho realizzato lato server per un recente progetto (opportunamente semplificata, chiaramente): Battezzata la mia action come un "POST" e un nome auto-esplicativo, subito è da notare che il valore di uscita è un "Task": questo nel puro spirito di "asincronicità" che è richiesto ad un servizio che eroga dati. Comunque...il codice è abbastanza chiaro: faccio un paio di verifiche sul MIME del contenuto ricevuto, stabilisco il percorso della folder temporanea dove verrà scritto lo stream del file caricato in upload, quindi leggo i dati "form" dove è contenuto il mio identificativo per l'oggetto business a cui dovrò associare il file ricevuto (un generico "Document", in questo caso).

E' importante che il metodo sia in grado di rispondere al client seguendo la semantica HTTP quindi ritornare un 404 "NotFound" se l'oggetto richiesto non esiste, un eventuale 401 "Unauthorized" se l'utente connesso al sistema non ha i diritti per compiere l'operazione, ecc. Ho volutamente "tagliato" il discorso delle permissions per non perdere il focus del post...

In ultimo scorro i contenuti "FileData" del provider di gestione e recupero sia il contenuto binario del file (o dei files) contenuti nella request, sia il nome del file specificato dal client che ha inviato la richiesta stessa. Il resto è semplice: lettura del contenuto di stream e associazione ad una nuova entità "business" che conserverà il file (lo "Scan") e salvataggio della stessa su disco o sulla base dati previa validazione delle informazioni.

Una piccola nota a valle del salvataggio dell'oggetto "Scan". Il metodo "ApplicationServiceLayer.SaveScan" emette una lista di "ValidationResult" per ogni errore di salvataggio o validazione fallita sull'entità: è buona regola inviare queste validazioni al client con un codice 400 "BadRequest", aggiungendo alla response i messaggi di errore generati. Lato client li potrete facilmente intercettare e gestire (magari per mostrarli direttamente all'utente finale).

Una volta definita la action server, passiamo a quello che ci interessa di più: il client. Ma prima una rapida domanda: come gestiamo i differenti HTTP status codes (400, 401, 404, ecc) che vengono generati nel caso in cui non tutto andasse liscio? L'argomento è molto dibatutto, e la mia personalissima opinione è che in un ambiente come .NET, tutto quello che non va come dovrebbe andare, va tratto come un'eccezione.

Ma dobbiamo ricordarci anche che il client dovrebbe implementare il pattern "async/await" per non ledere la user experience dell'utente. Ho quindi implementato un'eccezione "non standard" in grado di recuperare quante più informazioni possibili dal server, in presenza di uno status code diverso da 200 "OK": Passiamo ora all'implementazione del client. Si, l'ho detto anche prima: ma stavolta tengo fede alla promessa... Il metodo deve essere estremamente semplice da utilizzare: bastarà istanziare la classe (che potrebbe tranquillamente essere statica), passare come parametri l'identificativo del documento a cui il file va associato, il nome del file e il suo contenuto in bytes...et voilà!

L'implementazione non è altrattanto banale: validazione dell'input (un "must" per un buon stile di programmazione), creazione dell' HttpRequestMessage - chiaramente con verb "POST" e indirizzo completo dell'endpoint della action che abbiamo lato server - e creazione del contenuto della request...la parte più "succosa".

Il corpo della richiesta va riempito con un MultipartFormDataContent: una comodissima classe .NET che permette di wrappare una serie di differenti informazioni da "impacchettare" ed inviare al server. Essa funge da scatola sia per i contenuti binari (il nostro file), sia per le informazioni di "form" (l'id del documento da associare). Basta crearne un'istanza passando una stringa di "boundary" (una sorta di separatore per gli elementi contenuti nel body della "multipart request"), quindi inserire i vari contenuti.

Il primo elemento da aggiungere al MultiPartFormDataContent è di tipo ByteArrayContent: servirà per contenere i dettagli del file che vogliamo inviare al server. Passando i bytes del contenuto e il nome del file, non dobbiamo dimenticare di importare il "Content-Type" nell'header del contenuto stesso: servirà al server per stabilire la "natura" dei dati che riceverà. Quindi aggiungiamo tutto l'elemento al "multipart" container specificando la stringa "File" come nome dell'elemento trasmesso.

In seconda battuta inseriamo un contenuto testuale con StringContent: anche in questo caso bisognerà specificare il valore dell'identificativo del documento come contenuto, delegando il "name" del contenuto stesso ("documentId") all'aggiunta di StringContent sul multipart.

Ora che il nostro body della request è pronto, è sufficiente aggiungerlo al messaggio creato in precedenza, istanziare il client HttpClient, quindi inviare il tutto lato server restando in attesa con la keyword "await". Come anticipato qualche riga sopra, la response potrebbe essere uno status code di successo (200 - OK) che determina un'operazione lato server andata completamente a buon fine, oppure uno dei codici di "fail" che abbiamo gestito nelle WebAPI.

In caso di fallimento è necessario far entrare in gioco l'eccezione custom che abbiamo realizzato prima: diamo in pasto la request al metodo GetWebApiException: Il contenuto della response ricevuto dal server sarà deserializzato dal formato JSON (usando la meravigliosa libreria Json.NET), quindi saranno estratti tutti gli eventuali messaggi di errori presenti nel flusso ricevuto. L'implemetazione di WebApiException sarà in grado di gestire in maniera tipizzata le informazioni, di fatto rendendole usabili per la visualizzazione sul client.

Come detto in precedenza, ritengo che uno status code non di successo debba essere gestito tramite un eccezione a livello client-side: sta quindi allo sviluppatore eseguire queste chiamate in blocchi try/catch, all'interno dei quali poter intercettare eccezioni di tipo WebApiException, e trattare il loro contenuto nella maniera più consona alla situazione.

Spesso, anche cose semplici come l'upload di un file su un server, possono diventare complicate se non si conosce perfettamente il funzionamento del protocollo HTTP. Spero che questi appunti - perchè solo di questo si tratta - possano essere utili a qualche collega che - come il sottoscritto - ha speso inutilmente ore per un'inezia...
M.

Commenti

Post popolari in questo blog

Cancellazione fisica vs cancellazione logica dei dati

RESTful Stress: misurare le performance di un servizio REST

Load tests, Stress tests e performance di un servizio REST