Przesyłanie plików

Przesyłanie plików

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

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 na multipart/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 w MemoryStream.

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

Podziel się artykułem na: