Wstęp
W tym artykule pokazuję, jak przesyłać pliki między aplikacjami webowymi. Myślę, że wiedza tutaj zawarta jest kompletna i dużo obszerniejsza niż w innych artykułach. Zobaczysz tutaj kompletne mechanizmy do przesyłania plików pożądane w każdej aplikacji.
UWAGA! Ze względu na prostotę i czytelność, kody przedstawione w tym artykule nie są zbyt czyste. Miej to na uwadze.
Na GitHubie znajdują się przykładowe projekty. W pliku readme.md jest instrukcja, która pokazuje jak je poprawnie uruchamiać.
Jeśli szukasz informacji jak przesłać formularzem plik z danymi w JSON, przeczytaj ten artykuł.
Spis treści
- Przesyłanie pliku na serwer w Razor Pages
- Niebezpieczeństwa – możliwe podatności na ataki
- Ochrona przed ponownym przesłaniem pliku
- Walidacja rozmiaru pliku w RazorPages/MVC
- Przesyłanie pliku na Azure Blob Storage
- Przesyłanie pliku w Blazor
- Pasek postępu
- Wyświetlanie pobranego obrazka w Blazor
- Walidacja pliku w Blazor
- Przesyłanie pliku do WebApi
Przesyłanie pliku na serwer w Razor Pages
Na początek zajmiemy się przesyłaniem plików w obrębie aplikacji Razor Pages. Analogicznie będzie w przypadku MVC.
Formularz
Przede wszystkim musisz pamiętać, że plik jest zawsze przesyłany formularzem. A więc na początek trzeba stworzyć odpowiedni formularz:
<form method="post" enctype="multipart/form-data">
<div>
<label for="file-input">Wybierz plik</label>
<input type="file" id="file-input" accept=".jpg" />
</div>
</form>
Po stronie HTML posługujemy się zwykłym inputem
o typie file
. Atrybut accept
pozwala ograniczyć typy plików tylko do tych, które chcemy zobaczyć w okienku do wyboru plików.
Ale ważna uwaga – pamiętaj że musisz ustawić atrybut
enctype
dla formularza namultipart/form-data
. W innym przypadku plik po prostu nie przejdzie.
Teraz pytanie, jak odebrać plik po stronie serwera? Służy do tego specjalny interfejs IFormFile
. Ten interfejs znajduje się w Nugecie: Microsoft.AspNetCore.Http
, więc zainstaluj go najpierw.
Model
Napiszmy teraz klasę, która będzie modelem dla tego formularza i prześle plik wraz z jakimiś innymi danymi:
public class FormData
{
[Required(ErrorMessage = "Musisz wybrać plik")]
public IFormFile? FileToUpload { get; set; }
[Required(ErrorMessage = "Musisz wpisać wiadomość")]
public string? Message { get; set; }
}
Zwróć uwagę, że oba pola są oznaczone jako Required
– będą wymagane. I tu mała dygresja.
Wszystkie pola, które nie są oznaczone jako nullable
– znakiem zapytania (a nullable
używane jest w projekcie) domyślnie są uznane za wymagane. Z tego wynika, że powyższy kod mógłbym zapisać tak:
public class FormData
{
public IFormFile FileToUpload { get; set; }
public string Message { get; set; }
}
Zauważ, że w drugim przykładzie przy typach pól nie ma znaku zapytania, który oznacza pole jako nullable
. W tym momencie te pola są domyślnie wymagane. I jeśli któreś z nich nie będzie wypełnione, formularz nie przejdzie walidacji.
Przesyłanie
Jeśli nie wiesz co robią atrybuty Required
i jak walidować formularz, koniecznie przeczytaj artykuł Walidacja danych w MVC.
To teraz dodamy ten model do strony i zaktualizujemy formularz:
index.cshtml.cs:
public class IndexModel : PageModel
{
[BindProperty]
public FormData Data { get; set; }
public async Task OnPostAsync()
{
}
}
index.cshtml:
<form method="post" enctype="multipart/form-data">
<div>
<label asp-for="Data.FileToUpload">Wybierz plik</label>
<input type="file" asp-for="Data.FileToUpload" accept=".jpg" />
<span asp-validation-for="Data.FileToUpload" class="text-danger"/>
</div>
<div>
<label asp-for="Data.Message">Wiadomość</label>
<input type="text" asp-for="Data.Message" />
<span asp-validation-for="Data.Message" class="text-danger" />
</div>
<div>
<button type="submit">Wyślij mnie</button>
</div>
</form>
@section Scripts
{
<partial name="_ValidationScriptsPartial" />
}
Jeśli nie wiesz, czym jest atrybut asp-validation-for
lub _ValidationScriptsPartial
, koniecznie przeczytaj artykuł o walidacji. Krótko mówiąc – jeśli jakiś warunek walidacyjny nie zostanie spełniony (np. [Required]
), to w tym spanie
pojawi się komunikat błędu.
Pamiętaj też, że _ValidationScriptsPartial
musisz umieścić na końcu strony. W przeciwnym razie mechanizm walidacji po stronie klienta może nie zadziałać poprawnie.
Odbieranie danych
Teraz już wystarczy tylko odebrać te dane. Robimy to w metodzie OnPostAsync
.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
return Page();
EnsureFilesDirExists();
var secureFileName = Path.GetFileName(Data.FileToUpload.FileName);
var filePath = Path.Combine("files", secureFileName);
using var stream = new FileStream(filePath, FileMode.CreateNew);
await Data.FileToUpload.CopyToAsync(stream);
return Page();
}
private void EnsureFilesDirExists()
{
if (!Directory.Exists("files"))
Directory.CreateDirectory("files");
}
Najpierw sprawdzam, czy stan modelu jest prawidłowy – po stronie serwera też musisz o to zadbać.
Następnie tworzę obiekt klasy FileStream
, do którego wkopiowuję otrzymany plik. Plik za sprawą tego strumienia ląduje na dysku.
Jeśli przesyłane jest kilka plików, zamiast pojedynczej właściwości IFormFile
użyj jakiejś formy kolekcji w swoim modelu, np. IEnumerable<IFormFile>,
czy też List<IFormFile>
.
Nazwa właściwości w modelu musi być taka sama jak wartość atrybutu name
w elemencie input
. Oczywiście, jeśli posługujesz się tylko RazorPages i domyślnym bindingiem, to masz to zagwarantowane.
Niebezpieczeństwa
Zapchanie pamięci i atak DoS
UWAGA! Nie używaj MemoryStream
do łapania plików po stronie serwera. Pamiętaj, że wszystko co jest w MemoryStream
znajduje się w pamięci. Jeśli na Twój serwer przesyłane są pliki, wtedy użycie MemoryStream
może doprowadzić do wyczerpania pamięci i BUM! Wszystko wybuchnie. Stworzysz też podatność na atak Denial Of Service.
Dlatego też zawsze w takiej sytuacji używaj FileStream
lub lepiej – jeśli masz taką możliwość – przesyłaj plik bezpośrednio na blob storage – o tym za chwilę.
Niebezpieczna nazwa pliku i przejęcie kontroli nad komputerem
Drugim zagrożeniem jest nazwa pliku. Zdziwiony? Nazwa pliku i plik mogą zostać tak spreparowane, żeby dobrać się do danych znajdujących się na serwerze. A nawet przejąć nad nim całkowitą kontrolę. Jeśli posłużysz się gołą nazwą pliku, która przychodzi w IFormFile.FileName
stworzysz sobie podatność na atak Path Traversal. Piszę o tym więcej w swojej książce Zabezpieczanie aplikacji internetowych w .NET – podstawy. Jest tam wyjaśniony ten atak wraz z przykładem. A także kilka innych. Polecam zerknąć 🙂
Aby uniknąć podatności, zastosuj po prostu nazwę pliku, którą zwróci Ci Path.GetFileName()
.
Dobre praktyki
Poza tym, co opisałem wyżej, warto jeszcze sprawdzić rozmiar przesyłanego pliku. Ten rozmiar masz we właściwości IFormFile.Length
. Daj jakieś maksimum na wielkość pliku. Jeśli przesłany plik jest za duży, to po prostu zwróć błąd.
Dobrze jest też uruchomić jakiś skaner antywirusowy na przesyłanym pliku, jeśli masz taką możliwość.
No i pamiętaj, żeby zawsze sprawdzać po stronie serwera co do Ciebie przychodzi, bo walidacje po stronie klienta można łatwo obejść.
Jeśli przesyłany plik jest naprawdę duży, to zapisuj go w osobnym tasku. Zwróć odpowiedź do klienta w stylu, że plik jest procesowany i wkrótce się pojawi.
Ochrona przed ponownym przesłaniem
Może się zdarzyć tak, że w przeglądarce ktoś zdąży zupełnym przypadkiem wcisnąć przycisk do wysyłania pliku dwa razy. Można się przed tym uchronić w prosty sposób. Wystarczy zablokować guzik po przesłaniu. Wymaga to jednak nieco JavaScriptu. W standardowym mechanizmie walidacji można to osiągnąć w taki sposób:
<form method="post" enctype="multipart/form-data" id="form" onsubmit="return submitHandler()">
<!-- treść formularza -->
<div>
<button id="submit-btn" type="submit">Wyślij mnie</button>
</div>
</form>
@section Scripts
{
<partial name="_ValidationScriptsPartial" />
<script type="text/javascript">
function submitHandler() {
let form = $("#form");
if (!form.validate().checkForm())
return false;
$("#submit-btn").disabled = true;
return true;
}
</script>
}
Tutaj nadałem ID formularzowi i przyciskowi do wysyłania. Dodatkowo dodałem obsługę zdarzenia onsubmit
dla formularza. W tym handlerze najpierw sprawdzam, czy formularz jest poprawnie zwalidowany. Wywołanie form.validate().checkForm()
nie powoduje kolejnej walidacji, tylko sprawdza, czy formularz jest zwalidowany. Jeśli nie jest (bo np. nie podałeś wszystkich wymaganych danych), wtedy zwracam false – formularz wtedy nie zostaje przesłany.
W przeciwnym razie blokuję guzik do wysyłania i zwracam true. Nie ma opcji podwójnego wciśnięcia tego przycisku. Taki mechanizm powinieneś stosować w każdym formularzu. Możesz też schować taki formularz i wyświetlić komunikat, że formularz jest wysyłany. Po prostu pokaż zamiast niego jakiegoś diva
z taką informacją.
Walidacja rozmiaru pliku
Tak jak pisałem wyżej – warto dać jakiś maksymalny rozmiar pliku. Można to dość łatwo zwalidować zarówno po stronie klienta jak i serwera – wystarczy użyć własnego atrybutu walidacyjnego 🙂 Przede wszystkim muszę Cię odesłać do tego artykułu, w którym opisałem jak tworzyć własne atrybuty walidacyjne.
Nie będę tutaj powtarzał tej wiedzy, więc warto żebyś go przeczytał, jeśli czegoś nie rozumiesz. Tutaj dam Ci gotowe rozwiązanie.
Atrybut może wyglądać tak:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,
AllowMultiple = false)]
public class MaxFileSizeAttribute : ValidationAttribute, IClientModelValidator
{
private readonly int _maxFileSize;
public MaxFileSizeAttribute(int maxFileSize)
{
_maxFileSize = maxFileSize;
}
public override bool IsValid(object value)
{
var file = value as IFormFile;
if (file == null)
return true;
return file.Length <= _maxFileSize;
}
public void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-filesize", ErrorMessageString);
MergeAttribute(context.Attributes, "data-val-filesize-maxsize", _maxFileSize.ToString());
}
private static void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (!attributes.ContainsKey(key))
{
attributes.Add(key, value);
}
}
}
Dodatkowo trzeba dodać walidację po stronie klienta. Ja robię to w pliku _ValidationScriptsPartial.cs
. Cały plik może wyglądać tak:
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script type="text/javascript">
$.validator.addMethod("filesize", function (value, element, param) {
if (element.type != "file")
return false;
for(let i = 0; i < element.files.length; i++)
if(element.files[i].size > param)
return false;
return true;
});
$.validator.unobtrusive.adapters.addSingleVal("filesize", "maxsize");
</script>
Teraz wystarczy już tylko użyć tego w modelu:
public class FormData
{
[Required(ErrorMessage = "Musisz wybrać plik")]
[MaxFileSize(1024 * 1024, ErrorMessage = "Plik jest za duży, maksymalna wielkość pliku to 1 MB")]
public IFormFile? FileToUpload { get; set; }
[Required(ErrorMessage = "Musisz wpisać wiadomość")]
public string? Message { get; set; }
}
Ważne! Pamiętaj że żeby walidacja działała, model na stronie musi być opatrzony atrybutem BindProperty
:
public class IndexModel : PageModel
{
[BindProperty]
public FormData Data { get; set; } = new();
public async Task<IActionResult> OnPostAsync()
{
}
}
Jeśli tego nie będzie, formularz może zadziałać poprawnie i przesłać dane, ale walidacja nie zostanie uruchomiona.
Przesyłanie pliku bezpośrednio na blob storage
BlobStorage jest to usługa Azure’owa do przechowywania plików. Czasem lepiej/łatwiej umieścić pewne pliki właśnie tam niż u siebie na serwerze. To nie jest artykuł o tym, więc nie piszę tutaj więcej w szczególności jak obsługować i tworzyć taki storage. Jeśli wiesz o co chodzi, to ten akapit może Ci się przydać. Jeśli nie – pewnego dnia na pewno to opiszę.
Generalnie wszystko sprowadza się do wywołania odpowiedniego przeciążenia metody UploadBlobAsync
z BlobContainerClient
:
await blobContainerClient.UploadBlobAsync(
trustedFilename, Data.FileToUpload.OpenReadStream());
W takiej sytuacji plik ląduje bezpośrednio na Twoim blob storage.
Przesyłanie pliku w Blazor
Tutaj sprawa wygląda nieco inaczej. Generalnie mamy gotowy komponent do pobrania pliku: InputFile
. Co więcej, ten komponent nie musi być częścią formularza. To jednak nieco zmienia sposób podejścia do zadania. Zacznijmy od tego jak w ogóle działa przesyłanie plików w Blazor:
<InputFile OnChange="FileChangeHandler" accept=".jpg" />
@code{
private async Task FileChangeHandler(InputFileChangeEventArgs args)
{
using var stream = args.File.OpenReadStream();
}
}
Po pierwsze mamy komponent InputFile
, którego kluczowym zdarzeniem jest OnChange
. Trzeba tutaj podać handlera, który dostanie przekazany plik.
Parametr metody posiada pole File
(typu IBrowserFile
). Znajduje się tam pierwszy dostępny plik. Jeśli pozwoliłeś na dodanie kilku plików, wtedy możesz je odczytać z metody GetMultipleFiles
, która zwraca Ci listę IBrowserFile
. To wszystko znajduje się w klasie InputFileChangeEventArgs
w przesłanym parametrze.
Interfejs IBrowserFile
zawiera metodę OpenReadStream
, która zwraca strumień z danymi pliku. I teraz uwaga nr 1. OpenReadStream
przyjmuje w parametrze maksymalny rozmiar pliku, który może przekazać. Domyślnie ten parametr wskazuje 512 KB. Jeśli plik w strumieniu będzie większy, wtedy metoda wybuchnie wyjątkiem.
UWAGA! Blazor WASM pracuje tylko po stronie klienta – w jego przeglądarce. Nie ma tutaj żadnego serwera. Daje nam to pewne ograniczenie – nie możesz zapisać pobranego pliku – Blazor nie ma dostępu do systemu plików. Dlatego też w przypadku Blazor nie możesz posłużyć się
FileStream
, jak to było wyżej. Tutaj musisz po prostu przechować plik albo w tablicy bajtów, albo wMemoryStream
.
Zobaczmy teraz w jaki sposób można zrobić pobieranie pliku z progress barem
.
Przesyłanie pliku z progress barem
<InputFile OnChange="FileChangeHandler" accept=".jpg" />
@if(ReadingFile)
{
<div>
<progress value="@DataRead" max="@FileSize" />
</div>
}
@code{
private long FileSize { get; set; }
private long DataRead { get; set; }
private bool ReadingFile { get; set; } = false;
private async Task FileChangeHandler(InputFileChangeEventArgs args)
{
try
{
FileSize = args.File.Size;
DataRead = 0;
ReadingFile = true;
byte[] buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(1024); //pobranie buforu min. 1024 bajty
using var stream = args.File.OpenReadStream(1024 * 1024); //ograniczenie wielkości pliku do 1 MB
using MemoryStream fileContent = new MemoryStream();
while(await stream.ReadAsync(buffer) is int read && read > 0)
{
DataRead += read;
fileContent.Write(buffer);
StateHasChanged();
}
}finally
{
ReadingFile = false;
}
}
}
Wszystko rozbija się o odczyt pliku w porcjach po 1024 bajty. Kolejne części pliku są zapisywane w strumieniu fileContent
. Przy każdym zapisywanym fragmencie aktualizuję ilość odczytanych danych (DataRead
), która jest aktualną wartością w progress barze
. Aby progress bar
się odświeżył, trzeba zawołać StateHasChanged
.
Na koniec odczytu cały plik będzie w strumieniu fileContent
.
Pokazanie pobranego obrazka
W związku z tym, że Blazor pracuje po stronie klienta, nie możemy pliku zapisać gdzieś i go pokazać. Najprościej zatem pokazać taki obrazek w postaci zakodowanej do Base64
. Robi się to w taki sposób, że odczytujemy dane obrazka (np. przekazujemy go do MemoryStream
lub tablicy bajtów), następnie zamieniamy na Base64
, a na koniec do tagu IMG
przekazujemy ten obrazek w takiej postaci:
<img src="data:image/jpeg;base64,==data==" />
Ten tajemniczy ciąg w atrybucie src możemy rozbić na poszczególne elementy:
data:image/jpeg;base64,==data==
- data – to określenie, że obrazek będzie w formie danych, a nie ścieżki do pliku
- image/jpeg – to określenie typu tych danych
- base64 – format danych i dane
Zwróć uwagę na różne separatory konkretnych elementów – po data
jest dwukropek, po content-type
jest średnik, a po base64
jest przecinek. Jasne, prawda? 🙂 Jeśli nie widzisz faktycznego obrazka tylko ikonkę zastępczą, sprawdź te separatory w pierwszej kolejności.
Jak to może wyglądać w kodzie?
<InputFile OnChange="FileChangeHandler" accept=".jpg;.png" />
@if(FileRead)
{
<div>
<img src="@Img64Data" width="128"/>
</div>
}
@code{
private bool FileRead { get; set; } = false;
private string Img64Data { get; set; }
private async Task FileChangeHandler(InputFileChangeEventArgs args)
{
using MemoryStream ms = new MemoryStream();
var stream = args.File.OpenReadStream(1024 * 1024);
await stream.CopyToAsync(ms);
var data = Convert.ToBase64String(ms.GetBuffer());
Img64Data = $"data:{args.File.ContentType};base64,{data}";
FileRead = true;
}
}
Zwróć uwagę, że content type
przekazywanego pliku wziąłem z parametru InputFileChangeEventArgs
. Mógłbym go wpisać na sztywno, ale wtedy byłby problem jeśli użytkownik wybrałby inny typ pliku. Png, svg, czy chociażby bmp.
Walidacja pliku po stronie klienta
W związku z tym, że plik nie musi być w Blazor częścią formularza i tak naprawdę nie ma żadnej walidacji, musimy posłużyć się małym mykiem.
Tak jak w kodzie powyżej, odczytamy plik do jakiejś zmiennej i będziemy walidować tę zmienną. To oznacza, że trzeba zmienić model widoku dla Blazor:
public class FormDataViewModel
{
[Required(AllowEmptyStrings = false, ErrorMessage = "Wybierz plik")]
[MinLength(1, ErrorMessage = "Plik nie może być pusty")]
[MaxLength(1024 * 1024, ErrorMessage = "Plik jest za duży")]
public byte[]? FileContent { get; set; }
[Required(ErrorMessage = "Musisz wpisać wiadomość")]
public string? Message { get; set; }
}
I zasadniczo taka walidacja jest prawie dobra. Problem w tym, że jeśli przekażemy zbyt duży plik, to nie będzie o tym informacji. Dlaczego? Przypomnij sobie, jak odczytujemy plik:
var stream = args.File.OpenReadStream(1024 * 1024);
Jeśli będzie większy niż 1MB, to wtedy OpenReadStream
rzuci wyjątek. Dlatego też powinniśmy sprawdzić w tym miejscu, czy plik nie jest za duży.
Zgłoszenie błędu można zrobić na kilka sposobów. Najprostszym z nich jest po prostu pokazanie jakiegoś spana
przy InputFile
z odpowiednim tekstem. Np.:
<label>
Wybierz plik:
<InputFile OnChange="FileChangeHandler" accept=".jpg;.png" />
<ValidationMessage For="@(() => Data.FileContent)" class="text-danger" />
<span class="text-danger" style="@ErrorSpanVisibility">@ErrorSpanText</span>
</label>
@code{
private string ErrorSpanVisibility { get; set; } = "visibility:hidden";
private string ErrorSpanText { get; set; }
private async Task FileChangeHandler(InputFileChangeEventArgs args)
{
const int maxFileSize = 1024 * 1024;
if(args.File.Size > maxFileSize)
{
ErrorSpanVisibility = "visibility:visible";
ErrorSpanText = "Plik jest za duży";
return;
}
ErrorSpanVisibility = "visibility:hidden";
//odczyt pliku
}
}
To jest najprostsze rozwiązanie, które pewnie sprawdzi się w większości przypadków. Jednak można to też zrobić inaczej, za pomocą ValidationMessage
. Wymaga to jednak zmiany sposobu walidacji formularza i wykracza poza ramy tego artykułu. Obiecuję, że napiszę osobny artykuł o dodatkowej walidacji w Blazor.
Przesyłanie pliku na serwer do WebApi
Skoro mamy już fajny przykład w Blazorze, wykorzystamy go do przesłania pliku do WebApi. A właściwie całego modelu. Pokażę Ci jak przesłać dane wraz z plikiem.
Przede wszystkim musisz pamiętać, że tak jak w przypadku MVC / Razor Pages, plik jest wysyłany formularzem. Zasadniczo – przesyłanie pliku na serwer zawsze sprowadza się do umieszczenia go w formularzu i wysłaniu tego formularza.
Odczyt po stronie Api
Na początek stwórzmy projekt WebApi. Następnie dodajmy model, który będzie wyglądał tak:
public class WebApiFormData
{
public IFormFile File { get; set; }
public string Message { get; set; }
}
Zwróć uwagę na starego znajomego – IFormFile
.
Teraz najlepsze, jedyne co musimy zrobić w kontrolerze w WebApi to:
[Route("api/[controller]")]
[ApiController]
public class FormController : ControllerBase
{
[HttpPost]
public IActionResult PostHandler([FromForm]WebApiFormData data)
{
if (!ModelState.IsValid)
return BadRequest();
return Ok();
}
}
I to wystarczy, żeby WebApi odpowiednio odebrało dane! Po tej stronie już niczego nie musimy robić. Czy to nie jest piękne?
UWAGA! Jeśli podczas wysyłania danych do WebApi otrzymujesz błąd 415 (Unsupported Media Type) sprawdź, czy na pewno pobierasz w WebApi dane z formularza – atrybut FromForm
.
Wysyłanie do API
Rejestracja HttpClientFactory
Teraz musimy te dane wysłać formularzem za pomocą HttpClient
. Zrobimy to z aplikacji Blazor, która była tworzona wyżej w tym artykule. Zacznijmy od rejestracji fabryki dla HttpClient
przy rejestracji serwisów:
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://localhost:7144/");
});
Tego klienta pobierzemy sobie w metodzie, która jest wywoływana po poprawnym przesłaniu formularza w Blazor:
[Inject]
public IHttpClientFactory HttpClientFactory { get; set; }
private async Task ValidSumbitHandler(EditContext ctx)
{
var client = HttpClientFactory.CreateClient("api");
}
Jeśli nie rozumiesz tego kodu, przeczytaj artykuł o tym jak posługiwać się HttpClient.
Protokół MultipartForm data
Teraz musimy utworzyć całe ciało wiadomości, którą będziemy wysyłać. Będzie to formularz złożony z części.
Każda część to tzw. content
. C# daje nam różne takie klasy jak np. StreamContent
, do którego możemy wrzucić strumień, ByteArrayContent
, do którego możemy podać tablicę, StringContent
, do którego wrzucamy stringa.
I tutaj uwaga. Pojedyncze żądanie może mieć tylko jeden content. Ale jest specjalny typ – MultipartFormDataContent
, który składa się z innych contentów. Ostatecznie wszystko jest przesyłane oczywiście jako strumień bajtów. A skąd wiadomo gdzie kończy się jedna zawartość a zaczyna druga?
Istnieje coś takiego jak Boundary
. Jest to string, który oddziela poszczególne części między sobą. Jest ustawiany automatycznie, ale sam też go możesz podać. Pamiętaj tylko, że taki string powinien być unikalny.
Tak może wyglądać takie żądanie:
POST http://localhost:7144/api/form/1 HTTP/1.1
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=----------moje-unikalne-boundary
----------moje-unikalne-boundary
Content-Disposition: form-data; name="caption"
Summer vacation
----------moje-unikalne-boundary
Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg"
Content-Type: image/jpeg
(dane binarne)
----------moje-unikalne-boundary
Najpierw stwórzmy i dodajmy zawartość pliku. Posłużymy się tutaj ByteArrayContent
, ponieważ trzymamy plik w tablicy bajtów:
var request = new HttpRequestMessage(HttpMethod.Post, "api/form");
var form = new MultipartFormDataContent();
var fileContent = new ByteArrayContent(Data.FileContent);
form.Add(fileContent, nameof(WebApiFormData.File), "uploaded.jpg");
Na początku utworzyłem HttpRequestMessage
, który ostatecznie zostanie wysłany. To on będzie miał zawarty w sobie formularz.
Potem tworzę formularz, do którego będą dodawane poszczególne zawartości.
I tak tworzymy fileContent
z zawartością pliku, który został odczytany. Podczas dodawania tego contentu (form.Add
) ważne jest wszystko – nazwa tego contentu – file
– taka sama jak w modelu, który będzie przyjmowany przez WebApi. No i nazwa pliku – „uploaded.jpg”. Nazwa pliku musi być dodana w przypadku przesyłania pliku. Tak naprawdę powinieneś dodać prawdziwe rozszerzenie. Ja tylko z czystego lenistwa i prostoty wpisałem na sztywno „jpg”. Pamiętaj, żeby dodawać faktyczne rozszerzenie.
To teraz zajmiemy się pozostałymi właściwościami:
var msgContent = new StringContent(Data.Message, new MediaTypeHeaderValue("text/plain"));
form.Add(msgContent, nameof(WebApiFormData.Message));
Tutaj wszystko wygląda analogicznie, z tym że posługuję się StringContent zamiast ByteArrayContent.
I tutaj istotna uwaga. Zawsze dobrze jest podać media-type
dla stringa, który przesyłasz. Może to być czysty tekst (text/plain
), może to być string z json’em (application/json
), cokolwiek. Może być też tak, że WebApi nie chce mieć przekazanego żadnego mime-type
albo wręcz przeciwnie – jakiś konkretny. W takim przypadku zastosuj to, co jest napisane w dokumentacji konkretnego WebApi.
Teraz można już dodać nasz formularz do żądania i wysłać je:
request.Content = form;
await client.SendAsync(request);
I tyle. Formularz został wysłany do API.
Przesłanie większej ilości danych z plikiem
Często trzeba przesłać większą ilość danych wraz z plikiem. Napisałem osobny artykuł o tym jak przesłać dane w JSON razem z plikiem. Zachęcam do przeczytania, bo mówię też tam o mechanizmie bindingu.
Dzięki za przeczytanie tego artykułu. Mam nadzieję, że wszystko jest jasne. Jeśli jednak znalazłeś jakiś błąd lub czegoś nie rozumiesz, koniecznie daj znać w komentarzu.
Obrazek artykułu: Obraz autorstwa upklyak na Freepik