Wysyłanie i pobieranie plików z Azure Blob Storage w aplikacji Blazor i ASP.NET Core

Programowanie

Przechowywanie plików w naszej aplikacji jest częstym wymaganiem, które można zaimplementować na różne sposoby. W tym artykule pokażę jak zaimplementować przechowywanie plików w Azure Blob Storage oraz w jaki sposób aplikacja Blazor (WebAssembly) oraz WebApi stworzone w ASP.NET Core może uzyskać do nich dostęp. Zaproponowane rozwiązanie wdrożyłem w swojej aplikacji SaaS easyRenti.

Wstęp

Na moim blogu rozpocząłem cykl artykułów „Z pamiętnika SaaSa”, w któym przestawiam swoje podejście do tworzenia aplikacji SaaS, a także opisuję decyzje, które podjąłem w przypadku wybranych kwestii. Ten artykuł jest również kontynuacją tego cyklu i dlatego punktem startowym będzie nasza testowa aplikacja NotesApp, stworzona na jego potrzeby. W tym artykule znajdziesz opis tej aplikacji.

Źródła naszej aplikacji z dodaną obsługą plików znajdują się tutaj:

GitHub: https://github.com/robocik/NotesApp_V2_WithFiles

Azure Blob Storage

Jest to usługa w chmurze Azure, która umożliwia przechowywanie dużej ilości danych binarnych, w tym plików. W związku z tym, iż nasza aplikacja SaaS od początku ma być hostowana w chmurze Microsoftu, więc trzymanie plików w Blob Storage jest naturalnym wyborem.
Pierwszym krokiem jest swtorzenie nowego Blob Storage dla naszej aplikacji i odpowiednie skonfigurowanie. W opisie repo znajdziesz skrócony opis jak to zrobić.

GitHub: https://github.com/robocik/NotesApp_V2_WithFiles

Operacje na plikach w aplikacji Blazor

Zanim przejdziemy do implementacji naszego rozwiązania, warto zadać sobie pytanie, w jaki sposób chcemy podejść do wysyłania i pobierania plików w naszej aplikacji Blazor, która działa jako WebAssembly. Przez to, że jest to program uruchomiony w przeglądarce klienta, nie powinien mieć bezpośredniego dostępu do Azure. Przez bezpośredni dostęp rozumiem umieszczenie w kodzie programu connection stringa do Azure Blob Storage (z nazwą użytkowika i hasłem). Jest to zdecydowanie zły pomysł, gdyż w łatwy sposób cyber przestępca mógłby dostać się do kodu Twojej aplikacji Blazor i odczytać te poufne dane. A wtedy, miałby nieograniczony dostęp do wszystkich plików w Blob Storage. Zdecydowanie nie tędy droga.

Blob Storage za pośrednictwem WebAPI

Naturalnym rozwiązaniem jest przesyłanie plików z aplikacji Blazor do naszego WebAPI i dopiero nasz serwer wysyła plik do chmury.

Wysyłanie pliku z aplikacji Blazor do Azure Blob Storage za pośrednictwem WebAPI

To rozwiązanie ma szereg plusów, ale także minusów:

Plusy:

  • Aplikacja kliencka nie ma dostępu do Azure, wszystkie operacje przechodzą przez serwer więc zwiększa się bezpieczeńśtwo naszych plików
  • Po wysłaniu pliku do WebAPI możemy dokonać walidacji lub przekonwertować plik na inny format zanim wyślemy go do chmury
  • Prostota implementacji – ponieważ plik jest wysyłany do naszego WebAPI, mozemy wykorzystać zalety tranzakcji bazodanowej. Np. najpierw wstawiamy do bazy informacje o nowym pliku, a następnie wysyłamy go do Blob Storage. W przypadku wyjątku, bez problemu możemy wycofać wszystkie zmiany.

Minusy:

  • Wydajność – w przypadku wysyłania dużych plików, wysyłamy je dwukrotnie: najpierw z aplikacji Blazor do WebAPI, a potem z WebAPI do Azure Blob Storage. Cała operacja wolniej trwa, a po drugie obciążamy nasz serwer obsługą tego żądania.

Dobrze, jakby była możliwość, aby aplikacja Blazor wysyłałą plik bezpośrednio do Blob Storage z pominięciem naszego WebAPI. Całe szczęście Azure Blob Storage oferuje taką możliwość w bezpieczny sposób.

Blob Storage za pośrednictwem tokena SAS

Korzystając z tej funkcji, możemy wysłać plik z aplikacji Blazor bezpośrednio do Blob Storage. Polega to na tym, że:

  1. Wywołujemy operację w naszym WebAPI, która tworzy tymczasowy link do Blob Storage, który umożliwia wysłanie pliku. Ten link ma bardzo krótki czas życia (konfigurowalny) oraz jego zakres też jest ograniczony do konkretnego folderu lub nawet pliku.
  2. Blazor korzystając z tego adresu, wysyła plik do Blob Storage
Wysyłanie pliku z aplikacji Blazor do Azure Blob Storage za pośrednictwem tokenu SAS

Jak zawsze to podejście ma wady i zalety:

Zalety:

  • Wydajność – dzięki temu, że duże pliki są wysyłane bezpośrednio do Blob Storage (z pominięciem naszego WebAPI), odciążamy nasz serwer (nie musi przetwarzać takiego dużego żądania), zwiększa się dzięki temu skalowalność systemu.
  • Zmniejszone koszta – w przypadku chmury Azure, płacimy za moc obliczeniową. Im więcej nasze WebAPI będzie przetwarzać żądań, tym więcej zapłacimy. Dzięki SaS, requesty z plikami idą od razu do Blob Storage i pomijają WebAPI.

Wady:

  • Złożoność – zaimplementowanie tego rozwiązania wprowadza wiele dodatkowych problemów. Zwykle chcąc wysłać plik, musimy prócz wygenerowania unikatowego URL, zapisać jeszcze informację o nowym pliku do bazy. Dopiero wtedy aplikacja Blazor wyśle plik do chmury. W przypadku, gdy to wysłanie się nie powiedzie, należy zabdać o to, aby nasze WebAPI usunęło informację o tym pliku z bazy. Innymi słowy, w tym rozwiązaniu zawsze istnieje ryzyko „rozjechania się” bazy i Blob Storage.

Które podejście wybrać?

Jak zwykle nie ma prostej odpowiedzi na to pytanie. Jeśli zależy Ci na prostocie rozwiązania (z punktu widzenia złożoności oraz czasu na implementację) to wybierz wysyłanie plików za pośrednictwem swojego WebAPI (podejście 1). Jeśli natomiast Twoim głównym celem jest wydajność i odciążenie serwera, to zdecydowanie wysyłanie za pośrednictwem tokenu SAS będzie lepszym rozwiązaniem (podejście 2).

W easyRenti (a także w naszej testowej aplikacji) zastosowałem oba:

  • WebAPI – ten sposób wykorzystuję do uploadu awatara użytkownika oraz loga jego firmy z dwóch powodów:
    1. Lepsza walidacja wysyłanego pliku – awatar musi być obrazkiem, w związku z tym musimy sprawdzić, czy plik, który użytkownik wskazał jako awatar jego profilu, jest faktycznie plikiem graficznym.
    2. Dostosowanie rozmiaru – w celu optymalizacji, zmniejszam rozmiary obrazka do 300px x 300px. W przypadku tokenu SAS, te walidacje i transformacje ciężko byłoby zaimplementować (nie obyłoby się bez Azure Functions)
  • Token SAS (shared access signatures) – wszystkie pozostałe pliki wysyłam w ten sposób. Tutaj już nie ma potrzeby dokonania specjalnej walidacji. Użytkownik może wysłać do chmury dowolny plik (dokument pdf, obraz, etc). Żadne konwersje też nie są dokonywane.

Architektura

Operacje na plikach (niezależnie czy mówimy o plikach na lokalnym dysku twardym, czy w Azure Blob Storage) warto jest wydzielić do osobnego modułu. Nasza logika biznesowa nie powinna operować na plikach w ogóle. Zamiast tego, tworzymy interface IFileSystemProvider i to za jego pośrednictwem w logice biznesowej operujemy na naszych plikach. Takie podejście ma szereg zalet:

  • Elastyczność – bardzo łatwo podmienić nasz „system plików” na inny. Teraz pliki trzymamy na Azure, ale bez problemu możemy dodać możliwość przechowywania plików lokalnie na dysku. W tym celu stworzymy nową klasę LocalFileSystemProvider, która implementuje nasz interface IFileSystemProvider
  • Testowalność – nie chcemy, aby nasze testy jednostkowe wysyłały jakiekolwiek pliki do chmury. Dzięki wprowadzeniu naszego interface’u, testy mogą korzystać z mocka.

Metadane

Dobrym pomysłem jest zapisywanie informacji o plikach użytkownika w bazie danych, dzięki temu wiele ficzerów możemy zaoferować w aplikacji. W naszym testowym projekcie wprowadziłem klasę File, która zawiera najważniejsze informacje o plikach:

    public class File:NHBase
    {
        public virtual Guid? ObjectId { get; set; }
        public virtual string FileName { get; set; } = null!;
        public virtual DateTime CreatedDateTime { get; set; }
        public virtual ApplicationUser? CreatedBy { get; set; } = null!;
        public virtual long Length { get; set; }
        public virtual bool IsDeleted { get; set; }
    }

Większość miejsc w naszym programie powinna operować wyłącznie na tych obiektach. Chcemy pokazać użytkownikowi listę dostępnych plików? Odczytujemy ją z bazy danych (ObjectId wskazuje na konkretną notatkę, do której plik przynależy). Dzięki takiemu podejściu, nie musimy odwoływać się do Blob Storage, a do tego mamy nieograniczone możliwości tworzenia różnego rodzaju zapytań, w stylu: lista użytkowników, którzy mają więcej niż 100 plików dodanych w ciągu ostatnich 2 miesięcy itp.

Zwróć uwagę na flagę IsDeleted. Gdy użytkownik usuwa plik (lub notatkę), my fizycznie w tym momencie nie usuwamy pliku z Azure, a jedynie ustawiamy flagę IsDeleted=true. Dzięki temu sama operacja usunięcia notatki dla użytkownika jest bardzo szybka. A sam plik możemy już usunąć w późniejszym czasie (np. w nocy).

Oczywiście logika biznesowa operuje (pobiera) wyłącznie na plikach, z flagą IsDeleted=false

Wysyłanie pliku

W momencie, gdy użytkownik chce wysłać plik, dzielimy tą operację na dwie części (zobacz implementację UploadFile w FileDataService w aplikacji Blazor).

        public async Task<Guid> UploadFile(FileMetaData fileParam, Stream fileContent, 
        Action<long, long>? progressCallback)
        {
            return await Execute(async httpClient =>
            {
                var length = fileParam.FileLength;
                //first send info about a new file to the WebAPI
                var result = await httpClient.PostAsJsonAsync("api/files", fileParam, CreateOptions());
                if (!result.IsSuccessStatusCode)
                {
                    await ConvertToException(result);
                }
                result.EnsureSuccessStatusCode();
                var fileAccessToken = await result.Content.ReadAsAsync<FileAccessToken>();
                try
                {
                    var progress = new Progress<long>((progress) =>
                    {
                        progressCallback?.Invoke(progress, length);
                    });
                    //now send a content of the file to Azure Blob Storage
                    await _fileUploader.Upload(fileContent, progress, fileAccessToken);
                    return fileAccessToken.FileId;
                }
                catch (Exception ex)
                {
                    if (ex.InnerException != null)
                    {
                        await DeleteFile(fileAccessToken.FileId);
                    }
                    throw;
                }
            });
        }

Po pierwsze wysyłamy żądanie do naszego WebAPI, aby zapisać do bazy metadane (obiekt File) reprezentujące nowy plik. W parametrze przekazujemy wszystkie potrzebne informacje, np. jaki jest rozmiar pliku. Po stronie serwera przeprowadzamy wstępną walidację tych danych. Jeśli nie chcemy, aby użytkownik wysyłał pliki większe niż np 5MB, to tutaj możemy to sprawdzić i rzucić wyjątek. Tak samo możemy zweryfikować, czy użytkownik ma jeszcze wolne miejsce na swoim koncie (zakładam, że w naszej aplikacji SaaS ilość dostępnego miejsca zależy od tego czy jest to konto Free czy Premium). Pliki w Blob Storage nie mają oryginalnej nazwy, a guid, który jest Id obiektu File powiązanym z tym plikiem. Natomiast oryginalna nazwa pliku jest trzymana w bazie danych. Dzięki temu nie musimy przejmować się konfliktami nazw (gdy użytkownik wyśle do chmury 2 pliki o tej samej nazwie itp).

    public class UploadFileHandler : QueryHandlerBase<UploadFileCommand, FileAccessToken>
    {
        private readonly IFileSystemProvider _fileService;

        public UploadFileHandler(ISession session, SecurityInfo securityInfo, IMapper mapper, IDateTimeProvider dateTimeProvider, IFileSystemProvider fileService)
            : base(session, securityInfo, mapper, dateTimeProvider)
        {
            _fileService = fileService;
        }

        protected override async Task<FileAccessToken> Execute(ISession session, UploadFileCommand param)
        {
            var file = await CreateFile(session, param);

            var fileAccessToken= await _fileService.GenerateUploadToken(_securityInfo.User.Id, file.Id.ToString());
            return _mapper.Map< FileAccessToken>(fileAccessToken);
        }

        public async Task<File> CreateFile(ISession session, UploadFileCommand param)
        {
            var hasFiles = await Validate(session, param).ConfigureAwait(false);

            var file = new File
            {
                CreatedBy = _securityInfo.User,
                FileName = param.FileName,
                CreatedDateTime = _dateTimeProvider.UtcNow,
                Length = param.FileLength,
                ObjectId = param.ObjectId,
            };

            await session.SaveAsync(file).ConfigureAwait(false);
            hasFiles.Files.Add(file);
            await session.UpdateAsync(hasFiles).ConfigureAwait(false);
            return file;
        }

        private async Task<IHasFiles> Validate(ISession session, UploadFileCommand param)
        {
            if (param.FileLength > Constants.MaxFileSize)
            {
                throw new ArgumentOutOfRangeException("File is too big");
            }

            IHasFiles? hasFiles = await session.GetAsync<Note>(param.ObjectId).ConfigureAwait(false);

            if (hasFiles == null)
            {
                throw new NoteBookApp.Shared.Exceptions.ObjectNotFoundException();
            }

            var diskSize = Constants.OneSizeMB + 100; //this can be taken from Account settings. For testing, we assume that user has 100 MB disk available
            var allFileSize = await GetAllFileSize(session, _securityInfo.User).ConfigureAwait(false);
            if (allFileSize + param.FileLength > diskSize)
            {
                throw new InvalidOperationException("No free space available");
            }

            return hasFiles;
        }

        public async Task<long> GetAllFileSize(ISession session, ApplicationUser user, Guid? parentId = null)
        {
            var query = session.QueryOver<File>().Where(x => x.CreatedBy == user && !x.IsDeleted);
            if (parentId != null)
            {
                query = query.Where(x => x.ObjectId == parentId);
            }
            var allFileSize = await query
                .Select(Projections.Sum<File>(x => x.Length)).SingleOrDefaultAsync<long>().ConfigureAwait(false);
            return allFileSize;
        }
    }

Ważne!

Powyższa walidacja nie jest całkowicie bezpieczna. Pamiętaj, że wszystkie dane wysyłane z aplikacji klienckiej do WebAPI mogą być zmanipulowane przez oszusta (np. rozmiar pliku może być sztucznie zmodyfikowany). Dlatego bądź tego świadom i jeśli potrzebujesz mieć w 100% bezpieczną walidację, musisz wybrać inne podejście.

Drugim etapem jest wysłanie samego pliku do Blob Storage. Zwróć uwagę na obsługę błędów w metodzie UploadFile. Gdy wysyłanie do Azure się nie powiedzie, w bloku catch wywołuje metodę do usuwania pliku (żeby usunąć metadane, które przed chwilą zapisaliśmy w bazie).

Pobieranie pliku

Pobieranie pliku można zaimplementować na różne sposoby, które zależą między innmi od tego, czy dany plik jest dostępny publicznie, czy też nie. W przypadku publicznie dostępnych plików (czyli takich, gdzie dostęp jest możliwy dla innych użytkowników aplikacji, np. zdjęcie profilowe użytkownika), najprościej ustawić prawa dostępu do kontenera (folderu) Blob Storage na Public i wtedy wystarczy podawać linka do pliku (link do Blob Storage). I w sumie tyle.

Większy jednak problem jest z plikami, do których nie ma publicznego dostępu (Blob Storage jest ustawiony na Private). Wtedy podanie linka do pliku nie zadziała. Tutaj znowu możemy pobierać plik najpierw do naszego WebAPI (które ma dostęp do całego Azure), a następnie z WebAPI do aplikacji Blazor (wady i zalety podobne jak przy wysyłaniu pliku za pośrednictwem WebAPI).

Jednak można zastosować bardziej optymalne podejście z wykorzystaniem tokenu SAS, czyli aplikacja kliencka ściąga plik bezpośrednio z Azure. W tym celu Blazor wysyła żądanie ściągnięcia pliku do WebAPI, a to zwraca mu specjalny link do Blob Storage z czasowym tokenem SAS. Następnie Blazor za pomocą tego adresu URL pobiera plik.

Metoda do pobrania pliku na kliencie wygląda tak:

        private async Task DownloadFileAsync(Guid fileId)
        {
            try
            {
                var url = await FileDataService.GetFileDirectUrl(fileId);
                await JSRuntime.InvokeVoidAsync("DownloadFile", url);
            }
            catch (ObjectNotFoundException ex)
            {
                ShowError("Plik już nie istnieje.", ex);
                await refreshFiles();
            }
            catch (Exception ex)
            {
                ShowError("Nie udało się pobrać pliku. Proszę spróbować ponownie.", ex);
            }
        }

W powyższym kodzie wywołujemy metodę GetFileDirectUrl, która zwraca nam linka z tokenem SAS do Blob Storage, a następnie odpalamy specjalny skypt js, który wymusi na przeglądarce pobranie tego pliku. Sam skrypt wygląda tak:

        function DownloadFile(fileUrl) {
            var link = document.createElement('a');
            link.href = fileUrl;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

Jak wspomniałem, wygenerowany link do Azura jest ważny tylko przez określony czas (w naszym programie jest to 20 min). Warto ustawić czas wygaśnięcia, gdyż dzięki temu zmniejszamy ryzyko, że ktoś będzię mógł przejąć ten URL i ściągnać plik poza naszą aplikacją.

Usuwanie pliku

W naszej aplikacji mamy dwie metody usuwania plików:

  • Usunięcie pojedyńczego pliku
  • Usunięcie całej notatki, do której były dodane pliki. Powoduje to usunięcie wszystkich plików z nią powiązanych.

W celu zobrazowania różnych podejść do tego problemu, w naszej aplikacji operacja usuwania jednego pliku od razu ten plik usuwa z bazy i z Blob Storage.

    public class DeleteFileHandler : HandlerBase<DeleteFileCommand>
    {
        public DeleteFileHandler(ISession session, SecurityInfo securityInfo, IMapper mapper, IDateTimeProvider dateTimeProvider, IFileSystemProvider fileService)
            : base(session, securityInfo, mapper, dateTimeProvider, fileService)
        {
        }

        protected override async Task Execute(ISession session, DeleteFileCommand param)
        {
            var file = await session.QueryOver<File>().Where(x=>x.Id==param.Id).SingleOrDefaultAsync().ConfigureAwait(false);
            if (file == null)
            {
                throw new NoteBookApp.Shared.Exceptions.ObjectNotFoundException("File not found");
            }

            await DeleteFile(session, file).ConfigureAwait(false);
        }

        public async Task DeleteFile(ISession session, File file)
        {
            await session.DeleteAsync(file).ConfigureAwait(false);
            var data = file.ToFileIdentifier();
            await FileSystemProvider.Delete(data).ConfigureAwait(false);
        }
    }

W tym przypadku nie ma problemu, aby usunać jeden plik z Azure, gdyż jest to operacja w miarę szybka.

Jednak w przypadku usuwania notatki, może być konieczność usunięcia wielu plików i ta operacja może już trochę trwać. Chcąc zatem oszczędzić użytkownikowi czekania na zakończenie tej operacji, nasz kod tylko oznacza wszystkie obiekty File, że są do usunięcia (własność IsDeleted=true) i kończy pracę. Fizycznie pliki cały czas są jeszcze w Azure, ale nasza aplikacja będzie już je pomijać. Dla użytkownika są one już niedostępne.

Pozostaje tylko w dogodnym momencie usunąć wszystkie pliki, które mają ustawioną flagę IsDeleted. W easyRenti wykorzystałem Azure Functions, która uruchamia się raz na dobę (w nocy) i takie pliki usuwa. W naszym jednak przykładzie pominąłem ten etap, gdyż planuje napisać odzielny artykuł na ten temat.

Co można jeszcze usprawnić?

Przy opisie wysyłania plików wspomniałem, że przeprowadzona walidacja rozmiaru pliku lub dostępnego miejsca nie jest idealna i w przypadku, gdy potrzeba nam pewniejszego sprawdzenia, to należy zaimplementować to inaczej. Łatwo powiedzieć, trudniej zrobić.

Najprościej w takim przypadku wysyłać plik do naszego WebAPI, tam przeprowadzić walidację i dopiero z WebAPI wysłać do Azura.

Innym sposobem jest wykorzystanie Azure Functions do walidacji pliku już po jego wysłaniu do Blob Storage. Czyli aplikacja Blazor wysyła plik w taki sposób jak zaimplementowaliśmy w naszej aplikacji NotesApp. Następnie tworzymy funkcję w Azure Functions, która wykrywa moment zapisu pliku lub po prostu raz dziennie sprawdzi ostatnio dodane pliki, czy spełniają nasze kryteria (np rozmiar pliku). I jeśli wykryje coś nieprawidłowego, to po prostu ten plik usunie.

Zdjęcie profilowe

Obsługa awatara profilu wygląda inaczej. Po pierwsze profil może mieć tylko jeden obrazek, więc nie ma potrzeby tworzyć obiektu File, który będzie go reprezentował. Dodatkowo, obrazek profilowy jest publicznie dostępny (w katalogu z ustawieniem Public na Blob Storage), także bardzo łatwo stworzyć URL do takiego obrazka.

Tak wygląda bazowa metoda składająca URL do pliku:

public string GetFileUrl(string container, string file, string? param = null)
{
    return $"{_blobServiceClient.Uri}{container}/{file}?{param}";
}

Do klasy ApplicationUser reprezentującej użytkownika dodałem właśność AvatarFile:

    public class ApplicationUser : IdentityUser
    {
        public virtual long? AvatarFile { get; set; }

        public virtual FileIdentifier ToFileIdentifier()
        {
            var data = new FileIdentifier(FileMetaInfo.ProfileAwatarsFolder, Id);
            return data;
        }
    }

Jak możesz zauważyć typ tej własności to long. Jak zatem to działa? Obrazek dla profilu ma nazwę taką samą jak Id użytkownika, więc bardzo prosto stworzyć URL do awatara:

profile.AvatarUrl = FileSystemProvider.GetFileUrl(FileMetaInfo.ProfileAwatarsFolder, user.Id, user.AvatarFile.ToString());

Przykładowy adres URL do obrazka profilowego wygląda mniej więcej tak:

https://azurestorage.com/profile/0ba278eb-57a0-4265-adb0-bd88a6dd5f92?123456

gdzie:

  • 0ba278eb-57a0-4265-adb0-bd88a6dd5f92 – id naszego profilu (obiektu ApplicationUser)
  • 123456 – znacznik czasowy z AvatarFile

AvatarFile wskazuje na czas dodania obrazka do profilu. Dzięki temu, za każdym razem jak użytkownik zmieni obrazek, to także zmieni się adres URL (bo wartość z AvatarFile dodaję jako parametr na końcu adresu URL). Dzięki takiemu prostemu zabiegowi, mamy gwarancję, że obrazek zostanie zawsze odświeżony.

Wysyłanie zdjęcia profilowego

W tym przypadku zdjęcie jest wysyłane najpierw do naszego WebAPI. Tam po dokonaniu walidacji, zmieniamy jego rozmiar na 300px X 300px i dopiero wtedy wysyłamy do Blob Storage

    public class UploadAvatarFullHandler : HandlerBase<UploadAvatarFullCommand>
    {
        private readonly IImageResizer _imageResizer;

        public UploadAvatarFullHandler(ISession session, SecurityInfo securityInfo, IMapper mapper, IDateTimeProvider dateTimeProvider, IFileSystemProvider fileService, IImageResizer imageResizer)
            : base(session, securityInfo, mapper, dateTimeProvider,fileService)
        {
            _imageResizer = imageResizer;
        }

        
        protected override async Task Execute(ISession session, UploadAvatarFullCommand param)
        {
            if (param.FileLength > Constants.AvatarSize)
            {
                throw new ArgumentOutOfRangeException("File is too big");
            }

            
            using var image =await _imageResizer.ResizeAsync(param.Content, 300, 300).ConfigureAwait(false);

            FileIdentifier file = _securityInfo.User.ToFileIdentifier();
            await FileSystemProvider.Upload(image, file).ConfigureAwait(false);//upload to Azure

            var user = await session.GetAsync<ApplicationUser>(_securityInfo.User.Id).ConfigureAwait(false);
            user.AvatarFile = _dateTimeProvider.UtcNow.Ticks;// add current time (ticks) to have a unique number added as a parameter to avatar URL
            await session.UpdateAsync(user).ConfigureAwait(false);
        }

        
    }

Podsumowanie

Przechowywanie plików w Azure Blob Storage jest naturalnym wyborem w przypadku hostowania naszej aplikacji w chmurze Microsoftu. Samo wysyłanie plików możemy zaimplementować na różne sposoby: prosty, czyli za pośrednictwem naszego WebAPI lub bezpośrednio z aplikacji Blazor za pomocą tokena SAS. Który sposób wybrać zależy od naszych wymagań.

Dla soloprogramisty często najlepszym sposobem będzie ten najprostszy, czyli z wykorzystaniem WebAPI, natomiast w przypadku aplikacji SaaS, gdzie spodziewamy się większego obciążenia – dobrym wyborem będzie wykorzystanie bezpośredniego linku z tokenem SAS.

Jeśli masz inny pomysł, jak dokonać walidacji wysyłanego pliku za pomocą tokena SAS, to daj znać w komentarzach.

Share