AspNetCore.Docs/aspnetcore/razor-pages/upload-files.md

343 lines
15 KiB
Markdown
Raw Normal View History

---
title: Upload files to a Razor Page in ASP.NET Core
author: guardrex
description: Learn how to upload files to a Razor Page in ASP.NET Core using the FileUpload class.
2018-04-11 06:06:39 +08:00
monikerRange: '>= aspnetcore-2.0'
2018-01-29 23:21:31 +08:00
ms.author: riande
ms.date: 11/10/2018
2019-04-23 04:00:59 +08:00
ms.custom: "mvc, seodec18"
uid: razor-pages/upload-files
---
# Upload files to a Razor Page in ASP.NET Core
By [Luke Latham](https://github.com/guardrex)
This topic builds upon the [sample app](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/tutorials/razor-pages/razor-pages-start/sample) in <xref:tutorials/razor-pages/razor-pages-start>.
This topic shows how to use simple model binding to upload files, which works well for uploading small files. For information on streaming large files, see [Uploading large files with streaming](xref:mvc/models/file-uploads#uploading-large-files-with-streaming).
In the following steps, a movie schedule file upload feature is added to the sample app. A movie schedule is represented by a `Schedule` class. The class includes two versions of the schedule. One version is provided to customers, `PublicSchedule`. The other version is used for company employees, `PrivateSchedule`. Each version is uploaded as a separate file. The tutorial demonstrates how to perform two file uploads from a page with a single POST to the server.
[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/razor-pages/upload-files/samples) ([how to download](xref:index#how-to-download-a-sample))
## Security considerations
Caution must be taken when providing users with the ability to upload files to a server. Attackers may execute [denial of service](/windows-hardware/drivers/ifs/denial-of-service) and other attacks on a system. Some security steps that reduce the likelihood of a successful attack are:
* Upload files to a dedicated file upload area on the system, which makes it easier to impose security measures on uploaded content. When permitting file uploads, make sure that execute permissions are disabled on the upload location.
* Use a safe file name determined by the app, not from user input or the file name of the uploaded file.
* Only allow a specific set of approved file extensions.
* Verify client-side checks are performed on the server. Client-side checks are easy to circumvent.
* Check the size of the upload and prevent larger uploads than expected.
* Run a virus/malware scanner on uploaded content.
> [!WARNING]
> Uploading malicious code to a system is frequently the first step to executing code that can:
> * Completely takeover a system.
> * Overload a system with the result that the system completely fails.
> * Compromise user or system data.
> * Apply graffiti to a public interface.
## Add a FileUpload class
Create a Razor Page to handle a pair of file uploads. Add a `FileUpload` class, which is bound to the page to obtain the schedule data. Right click the *Models* folder. Select **Add** > **Class**. Name the class **FileUpload** and add the following properties:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/samples/2.x/RazorPagesMovie/Models/FileUpload.cs)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/samples/1.x/RazorPagesMovie/Models/FileUpload.cs)]
::: moniker-end
The class has a property for the schedule's title and a property for each of the two versions of the schedule. All three properties are required, and the title must be 3-60 characters long.
## Add a helper method to upload files
To avoid code duplication for processing uploaded schedule files, add a static helper method first. Create a *Utilities* folder in the app and add a *FileHelpers.cs* file with the following content. The helper method, `ProcessFormFile`, takes an [IFormFile](/dotnet/api/microsoft.aspnetcore.http.iformfile) and [ModelStateDictionary](/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) and returns a string containing the file's size and content. The content type and length are checked. If the file doesn't pass a validation check, an error is added to the `ModelState`.
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/samples/2.x/RazorPagesMovie/Utilities/FileHelpers.cs)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/samples/1.x/RazorPagesMovie/Utilities/FileHelpers.cs)]
::: moniker-end
### Save the file to disk
The sample app saves uploaded files into database fields. To save a file to disk, use a [FileStream](/dotnet/api/system.io.filestream). The following example copies a file held by `FileUpload.UploadPublicSchedule` to a `FileStream` in an `OnPostAsync` method. The `FileStream` writes the file to disk at the `<PATH-AND-FILE-NAME>` provided:
```csharp
public async Task<IActionResult> OnPostAsync()
{
// Perform an initial check to catch FileUpload class attribute violations.
if (!ModelState.IsValid)
{
return Page();
}
var filePath = "<PATH-AND-FILE-NAME>";
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await FileUpload.UploadPublicSchedule.CopyToAsync(fileStream);
}
return RedirectToPage("./Index");
}
```
The worker process must have write permissions to the location specified by `filePath`.
> [!NOTE]
> The `filePath` *must* include the file name. If the file name isn't provided, an [UnauthorizedAccessException](/dotnet/api/system.unauthorizedaccessexception) is thrown at runtime.
> [!WARNING]
> Never persist uploaded files in the same directory tree as the app.
>
> The code sample provides no server-side protection against malicious file uploads. For information on reducing the attack surface area when accepting files from users, see the following resources:
>
> * [Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload)
> * [Azure Security: Ensure appropriate controls are in place when accepting files from users](/azure/security/azure-security-threat-modeling-tool-input-validation#controls-users)
### Save the file to Azure Blob Storage
To upload file content to Azure Blob Storage, see [Get started with Azure Blob Storage using .NET](/azure/storage/blobs/storage-dotnet-how-to-use-blobs). The topic demonstrates how to use [UploadFromStream](/dotnet/api/microsoft.windowsazure.storage.file.cloudfile.uploadfromstreamasync) to save a [FileStream](/dotnet/api/system.io.filestream) to blob storage.
## Add the Schedule class
Right click the *Models* folder. Select **Add** > **Class**. Name the class **Schedule** and add the following properties:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/samples/2.x/RazorPagesMovie/Models/Schedule.cs)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/samples/1.x/RazorPagesMovie/Models/Schedule.cs)]
::: moniker-end
The class uses `Display` and `DisplayFormat` attributes, which produce friendly titles and formatting when the schedule data is rendered.
::: moniker range=">= aspnetcore-2.1"
## Update the RazorPagesMovieContext
Specify a `DbSet` in the `RazorPagesMovieContext` (*Data/RazorPagesMovieContext.cs*) for the schedules:
[!code-csharp[](upload-files/samples/2.x/RazorPagesMovie/Data/RazorPagesMovieContext.cs?highlight=17)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
## Update the MovieContext
Specify a `DbSet` in the `MovieContext` (*Models/MovieContext.cs*) for the schedules:
[!code-csharp[](upload-files/samples/1.x/RazorPagesMovie/Models/MovieContext.cs?highlight=13)]
::: moniker-end
## Add the Schedule table to the database
Open the Package Manger Console (PMC): **Tools** > **NuGet Package Manager** > **Package Manager Console**.
![PMC menu](upload-files/_static/pmc.png)
In the PMC, execute the following commands. These commands add a `Schedule` table to the database:
```powershell
Add-Migration AddScheduleTable
Update-Database
```
## Add a file upload Razor Page
In the *Pages* folder, create a *Schedules* folder. In the *Schedules* folder, create a page named *Index.cshtml* for uploading a schedule with the following content:
::: moniker range=">= aspnetcore-2.1"
[!code-cshtml[](upload-files/samples/2.x/RazorPagesMovie/Pages/Schedules/Index.cshtml)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-cshtml[](upload-files/samples/1.x/RazorPagesMovie/Pages/Schedules/Index.cshtml)]
::: moniker-end
Each form group includes a **\<label>** that displays the name of each class property. The `Display` attributes in the `FileUpload` model provide the display values for the labels. For example, the `UploadPublicSchedule` property's display name is set with `[Display(Name="Public Schedule")]` and thus displays "Public Schedule" in the label when the form renders.
Each form group includes a validation **\<span>**. If the user's input fails to meet the property attributes set in the `FileUpload` class or if any of the `ProcessFormFile` method file validation checks fail, the model fails to validate. When model validation fails, a helpful validation message is rendered to the user. For example, the `Title` property is annotated with `[Required]` and `[StringLength(60, MinimumLength = 3)]`. If the user fails to supply a title, they receive a message indicating that a value is required. If the user enters a value less than three characters or more than sixty characters, they receive a message indicating that the value has an incorrect length. If a file is provided that has no content, a message appears indicating that the file is empty.
## Add the page model
Add the page model (*Index.cshtml.cs*) to the *Schedules* folder:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/samples/2.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/samples/1.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs)]
::: moniker-end
The page model (`IndexModel` in *Index.cshtml.cs*) binds the `FileUpload` class:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/snapshot_samples/2.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet1)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/snapshot_samples/1.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet1)]
::: moniker-end
The model also uses a list of the schedules (`IList<Schedule>`) to display the schedules stored in the database on the page:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/snapshot_samples/2.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet2)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/snapshot_samples/1.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet2)]
::: moniker-end
When the page loads with `OnGetAsync`, `Schedules` is populated from the database and used to generate an HTML table of loaded schedules:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/snapshot_samples/2.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet3)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/snapshot_samples/1.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet3)]
::: moniker-end
When the form is posted to the server, the `ModelState` is checked. If invalid, `Schedule` is rebuilt, and the page renders with one or more validation messages stating why page validation failed. If valid, the `FileUpload` properties are used in *OnPostAsync* to complete the file upload for the two versions of the schedule and to create a new `Schedule` object to store the data. The schedule is then saved to the database:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/snapshot_samples/2.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet4)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/snapshot_samples/1.x/RazorPagesMovie/Pages/Schedules/Index.cshtml.cs?name=snippet4)]
::: moniker-end
## Link the file upload Razor Page
Open *Pages/Shared/_Layout.cshtml* and add a link to the navigation bar to reach the Schedules page:
```cshtml
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/Schedules/Index">Schedules</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Contact">Contact</a></li>
</ul>
</div>
```
## Add a page to confirm schedule deletion
When the user clicks to delete a schedule, a chance to cancel the operation is provided. Add a delete confirmation page (*Delete.cshtml*) to the *Schedules* folder:
::: moniker range=">= aspnetcore-2.1"
[!code-cshtml[](upload-files/samples/2.x/RazorPagesMovie/Pages/Schedules/Delete.cshtml)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-cshtml[](upload-files/samples/1.x/RazorPagesMovie/Pages/Schedules/Delete.cshtml)]
::: moniker-end
The page model (*Delete.cshtml.cs*) loads a single schedule identified by `id` in the request's route data. Add the *Delete.cshtml.cs* file to the *Schedules* folder:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/samples/2.x/RazorPagesMovie/Pages/Schedules/Delete.cshtml.cs)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/samples/1.x/RazorPagesMovie/Pages/Schedules/Delete.cshtml.cs)]
::: moniker-end
The `OnPostAsync` method handles deleting the schedule by its `id`:
::: moniker range=">= aspnetcore-2.1"
[!code-csharp[](upload-files/snapshot_samples/2.x/RazorPagesMovie/Pages/Schedules/Delete.cshtml.cs?name=snippet1&highlight=8,12-13)]
::: moniker-end
::: moniker range="= aspnetcore-2.0"
[!code-csharp[](upload-files/snapshot_samples/1.x/RazorPagesMovie/Pages/Schedules/Delete.cshtml.cs?name=snippet1&highlight=8,12-13)]
::: moniker-end
After successfully deleting the schedule, the `RedirectToPage` sends the user back to the schedules *Index.cshtml* page.
## The working Schedules Razor Page
When the page loads, labels and inputs for schedule title, public schedule, and private schedule are rendered with a submit button:
![Schedules Razor Page as seen on initial load with no validation errors and empty fields](upload-files/_static/browser1.png)
Selecting the **Upload** button without populating any of the fields violates the `[Required]` attributes on the model. The `ModelState` is invalid. The validation error messages are displayed to the user:
![Validation error messages appear next to each input control](upload-files/_static/browser2.png)
Type two letters into the **Title** field. The validation message changes to indicate that the title must be between 3-60 characters:
![Title validation message changed](upload-files/_static/browser3.png)
When one or more schedules are uploaded, the **Loaded Schedules** section renders the loaded schedules:
![Table of loaded schedules, showing each schedule's title, uploaded date in UTC, public version file size, and private version file size](upload-files/_static/browser4.png)
The user can click the **Delete** link from there to reach the delete confirmation view, where they have an opportunity to confirm or cancel the delete operation.
## Troubleshooting
2018-10-10 00:39:36 +08:00
For troubleshooting information with `IFormFile` uploading, see [File uploads in ASP.NET Core: Troubleshooting](xref:mvc/models/file-uploads#troubleshooting).