Skip to content

Commit e8166e8

Browse files
authored
File upload article enhancements (#34700)
1 parent 1192b7b commit e8166e8

File tree

3 files changed

+126
-7
lines changed

3 files changed

+126
-7
lines changed

aspnetcore/blazor/file-uploads.md

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,43 +45,53 @@ Rendered HTML:
4545
> [!NOTE]
4646
> In the preceding example, the `<input>` element's `_bl_2` attribute is used for Blazor's internal processing.
4747
48-
To read data from a user-selected file, call <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile.OpenReadStream%2A?displayProperty=nameWithType> on the file and read from the returned stream. For more information, see the [File streams](#file-streams) section.
48+
To read data from a user-selected file with a <xref:System.IO.Stream> that represents the file's bytes, call <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile.OpenReadStream%2A?displayProperty=nameWithType> on the file and read from the returned stream. For more information, see the [File streams](#file-streams) section.
4949

5050
<xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile.OpenReadStream%2A> enforces a maximum size in bytes of its <xref:System.IO.Stream>. Reading one file or multiple files larger than 500 KB results in an exception. This limit prevents developers from accidentally reading large files into memory. The `maxAllowedSize` parameter of <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile.OpenReadStream%2A> can be used to specify a larger size if required.
5151

52-
If you need access to a <xref:System.IO.Stream> that represents the file's bytes, use <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile.OpenReadStream%2A?displayProperty=nameWithType>. Avoid reading the incoming file stream directly into memory all at once. For example, don't copy all of the file's bytes into a <xref:System.IO.MemoryStream> or read the entire stream into a byte array all at once. These approaches can result in degraded app performance and potential [Denial of Service (DoS)](xref:blazor/security/interactive-server-side-rendering#denial-of-service-dos-attacks) risk, especially for server-side components. Instead, consider adopting either of the following approaches:
52+
Outside of processing a small file, avoid reading the incoming file stream directly into memory all at once. For example, don't copy all of the file's bytes into a <xref:System.IO.MemoryStream> or read the entire stream into a byte array all at once. These approaches can result in degraded app performance and potential [Denial of Service (DoS)](xref:blazor/security/interactive-server-side-rendering#denial-of-service-dos-attacks) risk, especially for server-side components. Instead, consider adopting either of the following approaches:
5353

5454
* Copy the stream directly to a file on disk without reading it into memory. Note that Blazor apps executing code on the server aren't able to access the client's file system directly.
5555
* Upload files from the client directly to an external service. For more information, see the [Upload files to an external service](#upload-files-to-an-external-service) section.
5656

57-
In the following examples, `browserFile` represents the uploaded file and implements <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile>. Working implementations for <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile> are shown in the file upload components later in this article.
57+
In the following examples, `browserFile` implements <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile> to represent an uploaded file. Working implementations for <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile> are shown in the file upload components later in this article.
58+
59+
When calling <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile.OpenReadStream%2A>, we recommend passing a maximum allowed file size in the `maxAllowedSize` parameter at the limit of the file sizes that you expect to receive. The default value is 500 KB. This article's examples use a maximum allowed file size variable or constant named `maxFileSize` but usually don't show setting a specific value.
5860

5961
<span aria-hidden="true">✔️</span><span class="visually-hidden">Supported:</span> The following approach is **recommended** because the file's <xref:System.IO.Stream> is provided directly to the consumer, a <xref:System.IO.FileStream> that creates the file at the provided path:
6062

6163
```csharp
6264
await using FileStream fs = new(path, FileMode.Create);
63-
await browserFile.OpenReadStream().CopyToAsync(fs);
65+
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(fs);
6466
```
6567

6668
<span aria-hidden="true">✔️</span><span class="visually-hidden">Supported:</span> The following approach is **recommended** for [Microsoft Azure Blob Storage](/azure/storage/blobs/storage-blobs-overview) because the file's <xref:System.IO.Stream> is provided directly to <xref:Azure.Storage.Blobs.BlobContainerClient.UploadBlobAsync%2A>:
6769

6870
```csharp
6971
await blobContainerClient.UploadBlobAsync(
70-
trustedFileName, browserFile.OpenReadStream());
72+
trustedFileName, browserFile.OpenReadStream(maxFileSize));
73+
```
74+
75+
<span aria-hidden="true">✔️</span><span class="visually-hidden">Only recommended for small files:</span> The following approach is only **recommended for small files** because the file's <xref:System.IO.Stream> content is read into a <xref:System.IO.MemoryStream> in memory (`memoryStream`), which incurs a performance penalty and [DoS](xref:blazor/security/interactive-server-side-rendering#denial-of-service-dos-attacks) risk. For an example that demonstrates this technique to save a thumbnail image with an <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile> to a database using [Entity Framework Core (EF Core)](/ef/core/), see the [Save small files directly to a database with EF Core](#save-small-files-directly-to-a-database-with-ef-core) section later in this article.
76+
77+
```csharp
78+
using var memoryStream = new MemoryStream();
79+
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
80+
var smallFileByteArray = memoryStream.ToArray();
7181
```
7282

7383
<span aria-hidden="true">❌</span><span class="visually-hidden">Not recommended:</span> The following approach is **NOT recommended** because the file's <xref:System.IO.Stream> content is read into a <xref:System.String> in memory (`reader`):
7484

7585
```csharp
7686
var reader =
77-
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
87+
await new StreamReader(browserFile.OpenReadStream(maxFileSize)).ReadToEndAsync();
7888
```
7989

8090
<span aria-hidden="true">❌</span><span class="visually-hidden">Not recommended:</span> The following approach is **NOT recommended** for [Microsoft Azure Blob Storage](/azure/storage/blobs/storage-blobs-overview) because the file's <xref:System.IO.Stream> content is copied into a <xref:System.IO.MemoryStream> in memory (`memoryStream`) before calling <xref:Azure.Storage.Blobs.BlobContainerClient.UploadBlobAsync%2A>:
8191

8292
```csharp
8393
var memoryStream = new MemoryStream();
84-
await browserFile.OpenReadStream().CopyToAsync(memoryStream);
94+
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
8595
await blobContainerClient.UploadBlobAsync(
8696
trustedFileName, memoryStream));
8797
```
@@ -873,6 +883,113 @@ The following `FileUpload4` component shows the complete example.
873883

874884
:::moniker-end
875885

886+
## Save small files directly to a database with EF Core
887+
888+
Many ASP.NET Core apps use [Entity Framework Core (EF Core)](/ef/core/) to manage database operations. Saving thumbnails and avatars directly to the database is a common requirement. This section demonstrates a general approach that can be further enhanced for production apps.
889+
890+
The following pattern:
891+
892+
* Is based on the [Blazor movie database tutorial app](xref:blazor/tutorials/movie-database-app/index).
893+
* Can be enhanced with additional code for file size and content type [validation feedback](xref:blazor/forms/validation).
894+
* Incurs a performance penalty and [DoS](xref:blazor/security/interactive-server-side-rendering#denial-of-service-dos-attacks) risk. Carefully weigh the risk when reading any file into memory and consider alternative approaches, especially for larger files. Alternative approaches include saving files directly to disk or a third-party service for antivirus/antimalware checks, further processing, and serving to clients.
895+
896+
For the following example to work in a Blazor Web App (ASP.NET Core 8.0 or later), the component must adopt an [interactive render mode](xref:blazor/fundamentals/index#static-and-interactive-rendering-concepts) (for example, `@rendermode InteractiveServer`) to call `HandleSelectedThumbnail` on an `InputFile` component file change (`OnChange` parameter/event). Blazor Server app components are always interactive and don't require a render mode.
897+
898+
In the following example, a small thumbnail (<= 100 KB) in an <xref:Microsoft.AspNetCore.Components.Forms.IBrowserFile> is saved to a database with EF Core. If a file isn't selected by the user for the `InputFile` component, a default thumbnail is saved to the database.
899+
900+
The default thumbnail (`default-thumbnail.jpg`) is at the project root with a **Copy to Output Directory** setting of **Copy if newer**:
901+
902+
![Default generic thumbnail image](~/blazor/file-uploads/_static/default-thumbnail.jpg)
903+
904+
The `Movie` model (`Movie.cs`) has a property (`Thumbnail`) to hold the thumbnail image data:
905+
906+
```csharp
907+
[Column(TypeName = "varbinary(MAX)")]
908+
public byte[]? Thumbnail { get; set; }
909+
```
910+
911+
Image data is stored as bytes in the database as [`varbinary(MAX)`](/sql/t-sql/data-types/binary-and-varbinary-transact-sql). The app base-64 encodes the bytes for display because base-64 encoded data is roughly a third larger than the raw bytes of the image, thus base-64 image data requires additional database storage and reduces the performance of database read/write operations.
912+
913+
Components that display the thumbnail pass image data to the `img` tag's `src` attribute as JPEG, base-64 encoded data:
914+
915+
```razor
916+
<img src="data:image/jpeg;base64,@Convert.ToBase64String(movie.Thumbnail)"
917+
alt="User thumbnail" />
918+
```
919+
920+
In the following `Create` component, an image upload is processed. You can enhance the example further with custom validation for file type and size using the approaches in <xref:blazor/forms/validation>. To see the full `Create` component without the thumbnail upload code in the following example, see the `BlazorWebAppMovies` sample app in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples).
921+
922+
`Components/Pages/MoviePages/Create.razor`:
923+
924+
```razor
925+
@page "/movies/create"
926+
@rendermode InteractiveServer
927+
@using Microsoft.EntityFrameworkCore
928+
@using BlazorWebAppMovies.Models
929+
@inject IDbContextFactory<BlazorWebAppMovies.Data.BlazorWebAppMoviesContext> DbFactory
930+
@inject NavigationManager NavigationManager
931+
932+
...
933+
934+
<div class="row">
935+
<div class="col-md-4">
936+
<EditForm method="post" Model="Movie" OnValidSubmit="AddMovie"
937+
FormName="create" Enhance>
938+
<DataAnnotationsValidator />
939+
<ValidationSummary class="text-danger" role="alert"/>
940+
941+
...
942+
943+
<div class="mb-3">
944+
<label for="thumbnail" class="form-label">Thumbnail:</label>
945+
<InputFile id="thumbnail" OnChange="HandleSelectedThumbnail"
946+
class="form-control" />
947+
</div>
948+
<button type="submit" class="btn btn-primary">Create</button>
949+
</EditForm>
950+
</div>
951+
</div>
952+
953+
...
954+
955+
@code {
956+
private const long maxFileSize = 102400;
957+
private IBrowserFile? browserFile;
958+
959+
[SupplyParameterFromForm]
960+
private Movie Movie { get; set; } = new();
961+
962+
private void HandleSelectedThumbnail(InputFileChangeEventArgs e)
963+
{
964+
browserFile = e.File;
965+
}
966+
967+
private async Task AddMovie()
968+
{
969+
using var context = DbFactory.CreateDbContext();
970+
971+
if (browserFile?.Size > 0 && browserFile?.Size <= maxFileSize)
972+
{
973+
using var memoryStream = new MemoryStream();
974+
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
975+
976+
Movie.Thumbnail = memoryStream.ToArray();
977+
}
978+
else
979+
{
980+
Movie.Thumbnail = File.ReadAllBytes(
981+
$"{AppDomain.CurrentDomain.BaseDirectory}default_thumbnail.jpg");
982+
}
983+
984+
context.Movie.Add(Movie);
985+
await context.SaveChangesAsync();
986+
NavigationManager.NavigateTo("/movies");
987+
}
988+
}
989+
```
990+
991+
The same approach would be adopted in the `Edit` component with an interactive render mode if users were allowed to edit a movie's thumbnail image.
992+
876993
## Upload files to an external service
877994

878995
Instead of an app handling file upload bytes and the app's server receiving uploaded files, clients can directly upload files to an external service. The app can safely process the files from the external service on demand. This approach hardens the app and its server against malicious attacks and potential performance problems.
19.2 KB
Loading

aspnetcore/blazor/tutorials/movie-database-app/part-8.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ If you're new to Blazor, we recommend reading the following Blazor articles that
285285
* <xref:blazor/host-and-deploy/index>
286286
* <xref:blazor/blazor-ef-core> covers concurrency with EF Core in Blazor apps.
287287

288+
For guidance on adding a thumbnail file upload feature to this tutorial's sample app, see <xref:blazor/file-uploads#save-small-files-directly-to-a-database-with-ef-core>.
289+
288290
In the documentation website's sidebar navigation, articles are organized by subject matter and laid out in roughly in a general-to-specific or basic-to-complex order. The best approach when starting to learn about Blazor is to read down the table of contents from top to bottom.
289291

290292
## Troubleshoot with the completed sample

0 commit comments

Comments
 (0)