From ed9f786f887e18bc81a5421a77564c32cddbae0d Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Mon, 22 May 2023 12:56:02 -1000 Subject: [PATCH] IForm binding /5 (#29312) * IForm binding /5 * IForm binding /5 * IForm binding /5 * IForm binding /5 * Apply suggestions from code review Co-authored-by: Safia Abdalla * IForm binding /5 --------- Co-authored-by: Safia Abdalla --- aspnetcore/fundamentals/minimal-apis.md | 5 +- ...meter-binding.md => parameter-binding7.md} | 3 + .../includes/parameter-binding8.md | 357 ++++++++++++++++++ .../minimal-apis/parameter-binding.md | 3 +- .../samples8/Iform/Iform.csproj | 9 + .../samples8/Iform/Program.cs | 59 +++ .../Iform/appsettings.Development.json | 8 + aspnetcore/release-notes/aspnetcore-8.0.md | 8 + 8 files changed, 450 insertions(+), 2 deletions(-) rename aspnetcore/fundamentals/minimal-apis/includes/{parameter-binding.md => parameter-binding7.md} (99%) create mode 100644 aspnetcore/fundamentals/minimal-apis/includes/parameter-binding8.md create mode 100644 aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Iform.csproj create mode 100644 aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Program.cs create mode 100644 aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/appsettings.Development.json diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index 46d4835c5f..f384a7fafa 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -62,7 +62,10 @@ The arguments passed to these methods are called "route h ## Parameter binding -[!INCLUDE [parameter binding](~/fundamentals/minimal-apis/includes/parameter-binding.md)] +[!INCLUDE [parameter binding](~/fundamentals/minimal-apis/includes/parameter-binding7.md)] + ### Read the request body diff --git a/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding.md b/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding7.md similarity index 99% rename from aspnetcore/fundamentals/minimal-apis/includes/parameter-binding.md rename to aspnetcore/fundamentals/minimal-apis/includes/parameter-binding7.md index 27ac5ac65f..9f9620133e 100644 --- a/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding.md +++ b/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding7.md @@ -1,3 +1,4 @@ +:::moniker range="= aspnetcore-7.0" Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers. A binding source determines where parameters are bound from. Binding sources can be explicit or inferred based on HTTP method and parameter type. Supported binding sources: @@ -337,3 +338,5 @@ Since the sample code configures both serialization and deserialization, it can :::code language="csharp" source="~/fundamentals/minimal-apis/7.0-samples/WebMinJson/Program.cs" id="snippet_readfromjsonasyncwithoptions" highlight="5-8,12"::: Since the preceding code applies the customized options only to deserialization, the output JSON excludes `NameField`. + +:::moniker-end diff --git a/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding8.md b/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding8.md new file mode 100644 index 0000000000..aa62e887a5 --- /dev/null +++ b/aspnetcore/fundamentals/minimal-apis/includes/parameter-binding8.md @@ -0,0 +1,357 @@ +:::moniker range=">= aspnetcore-8.0" + +Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers. A binding source determines where parameters are bound from. Binding sources can be explicit or inferred based on HTTP method and parameter type. + +Supported binding sources: + +* Route values +* Query string +* Header +* Body (as JSON) +* Services provided by dependency injection +* Custom + +Binding from form values is ***not*** natively supported in .NET. + +The following `GET` route handler uses some of these parameter binding sources: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_pbg&highlight=8-11)] + +The following table shows the relationship between the parameters used in the preceding example and the associated binding sources. + +| Parameter | Binding Source | +|--|--| +| `id` | route value | +| `page` | query string | +| `customHeader` | header | +| `service` | Provided by dependency injection | + +The HTTP methods `GET`, `HEAD`, `OPTIONS`, and `DELETE` don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, [bind explicitly](#explicit-parameter-binding) with [`[FromBody]`](xref:Microsoft.AspNetCore.Mvc.FromBodyAttribute) or read from the . + +The following example POST route handler uses a binding source of body (as JSON) for the `person` parameter: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_pbp&highlight=5)] + +The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following route handlers show how to read request data directly from the request: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Snippets/Program.cs?name=snippet_ManualRequestBinding&highlight=3-5,12)] + +### Explicit Parameter Binding + +Attributes can be used to explicitly declare where parameters are bound from. + + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_epb)] + +| Parameter | Binding Source | +| --------- | -------------- | +| `id` | route value with the name `id` | +| `page` | query string with the name `"p"`| +| `service` | Provided by dependency injection | +| `contentType` | header with the name `"Content-Type"` | + +> [!NOTE] +> Binding from form values is ***not*** natively supported in .NET. + +### Parameter binding with dependency injection + +Parameter binding for minimal APIs binds parameters through [dependency injection](xref:fundamentals/dependency-injection) when the type is configured as a service. It's not necessary to explicitly apply the [`[FromServices]`](xref:Microsoft.AspNetCore.Mvc.FromServicesAttribute) attribute to a parameter. In the following code, both actions return the time: + +[!code-csharp[](~/release-notes/aspnetcore-7/samples/ApiController/Program.cs?name=snippet_min)] + +### Optional parameters + +Parameters declared in route handlers are treated as required: + +* If a request matches the route, the route handler only runs if all required parameters are provided in the request. +* Failure to provide all required parameters results in an error. + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_op1)] + +| URI | result | +| --------- | -------------- | +| `/products?pageNumber=3` | 3 returned | +| `/products` | `BadHttpRequestException`: Required parameter "int pageNumber" was not provided from query string. | +| `/products/1` | HTTP 404 error, no matching route | + +To make `pageNumber` optional, define the type as optional or provide a default value: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_op2)] + +| URI | result | +| --------- | -------------- | +| `/products?pageNumber=3` | 3 returned | +| `/products` | 1 returned | +| `/products2` | 1 returned | + +The preceding nullable and default value applies to all sources: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_op3)] + +The preceding code calls the method with a null product if no request body is sent. + +**NOTE**: If invalid data is provided and the parameter is nullable, the route handler is ***not*** run. + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_op4)] + +| URI | result | +| --------- | -------------- | +| `/products?pageNumber=3` | `3` returned | +| `/products` | `1` returned | +| `/products?pageNumber=two` | `BadHttpRequestException`: Failed to bind parameter `"Nullable pageNumber"` from "two". | +| `/products/two` | HTTP 404 error, no matching route | + +See the [Binding Failures](#bf) section for more information. + +### Special types + +The following types are bound without explicit attributes: + +* : The context which holds all the information about the current HTTP request or response: + + ```csharp + app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World")); + ``` + +* and : The HTTP request and HTTP response: + + ```csharp + app.MapGet("/", (HttpRequest request, HttpResponse response) => + response.WriteAsync($"Hello World {request.Query["name"]}")); + ``` + +* : The cancellation token associated with the current HTTP request: + + ```csharp + app.MapGet("/", async (CancellationToken cancellationToken) => + await MakeLongRunningRequestAsync(cancellationToken)); + ``` + +* : The user associated with the request, bound from : + + ```csharp + app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name); + ``` + + + +#### Bind the request body as a `Stream` or `PipeReader` + +The request body can bind as a [`Stream`](/dotnet/api/system.io.stream) or [`PipeReader`](/dotnet/api/system.io.pipelines.pipereader) to efficiently support scenarios where the user has to process data and: + +* Store the data to blob storage or enqueue the data to a queue provider. +* Process the stored data with a worker process or cloud function. + +For example, the data might be enqueued to [Azure Queue storage](/azure/storage/queues/storage-queues-introduction) or stored in [Azure Blob storage](/azure/storage/blobs/storage-blobs-introduction). + +The following code implements a background queue: + +[!code-csharp[](~/fundamentals/minimal-apis/bindStreamPipeReader/7.0-samples/PipeStreamToBackgroundQueue/BackgroundQueueService.cs)] + +The following code binds the request body to a `Stream`: + +[!code-csharp[](~/fundamentals/minimal-apis/bindStreamPipeReader/7.0-samples/PipeStreamToBackgroundQueue/Program.cs?name=snippet_1)] + +The following code shows the complete `Program.cs` file: + +[!code-csharp[](~/fundamentals/minimal-apis/bindStreamPipeReader/7.0-samples/PipeStreamToBackgroundQueue/Program.cs?name=snippet)] + +* When reading data, the `Stream` is the same object as `HttpRequest.Body`. +* The request body isn't buffered by default. After the body is read, it's not rewindable. The stream can't be read multiple times. +* The `Stream` and `PipeReader` aren't usable outside of the minimal action handler as the underlying buffers will be disposed or reused. + + +#### File uploads using IFormFile and IFormFileCollection + +The following code uses and to upload file: + +:::code language="csharp" source="~/fundamentals/minimal-apis/iformFile/7.0-samples/MinimalApi/Program.cs" ::: + +Authenticated file upload requests are supported using an [Authorization header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Authorization), a [client certificate](/aspnet/core/security/authentication/certauth), or a cookie header. + +There is no built-in support for [antiforgery](/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0&preserve-view=true#anti7). However, it can be implemented using the [`IAntiforgery` service](/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0&preserve-view=true#antimin7). + + + +### Bind arrays and string values from headers and query strings + +The following code demonstrates binding query strings to an array of primitive types, string arrays, and [StringValues](/dotnet/api/microsoft.extensions.primitives.stringvalues): + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=snippet_bqs2pa)] + +Binding query strings or header values to an array of complex types is supported when the type has `TryParse` implemented. The following code binds to a string array and returns all the items with the specified tags: + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=snippet_bind_str_array)] + +The following code shows the model and the required `TryParse` implementation: + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=snippet_model)] + +The following code binds to an `int` array: + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=snippet_iaray)] + +To test the preceding code, add the following endpoint to populate the database with `Todo` items: + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=snippet_batch)] + +Use a tool like Postman to pass the following data to the previous endpoint: + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=batch_post_payload)] + +The following code binds to the header key `X-Todo-Id` and returns the `Todo` items with matching `Id` values: + +[!code-csharp[](~/fundamentals/minimal-apis/bindingArrays/7.0-samples/todo/Program.cs?name=snippet_getHeader)] + + + +### Parameter binding for argument lists with [AsParameters] + + enables simple parameter binding to types and not complex or recursive model binding. + +Consider the following code: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Program.cs" id="snippet_top"::: + +Consider the following `GET` endpoint: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Program.cs" id="snippet_id" highlight="2"::: + +The following `struct` can be used to replace the preceding highlighted parameters: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Models/TodoDb.cs" id="snippet" ::: + +The refactored `GET` endpoint uses the preceding `struct` with the [AsParameters](/dotnet/api/microsoft.aspnetcore.http.asparametersattribute?view=aspnetcore-7.0&preserve-view=true) attribute: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Program.cs" id="snippet_ap_id" highlight="2"::: + +The following code shows additional endpoints in the app: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Program.cs" id="snippet_post_put_delete" ::: + +The following classes are used to refactor the parameter lists: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Models/TodoDb.cs" id="snippet_1" ::: + +The following code shows the refactored endpoints using `AsParameters` and the preceding `struct` and classes: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Program.cs" id="snippet_ap_post_put_delete" ::: + +The following [`record`](/dotnet/csharp/language-reference/builtin-types/record) types can be used to replace the preceding parameters: + +:::code language="csharp" source="~/../AspNetCore.Docs.Samples/fundamentals/minimal-apis/samples/arg-lists/Models/TodoRecord.cs" id="snippet_1" ::: + +Using a `struct` with `AsParameters` can be more performant than using a `record` type. + +The [complete sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/arg-lists) in the [AspNetCore.Docs.Samples](https://github.com/dotnet/AspNetCore.Docs.Samples) repository. + +### Custom Binding + +There are two ways to customize parameter binding: + +1. For route, query, and header binding sources, bind custom types by adding a static `TryParse` method for the type. +1. Control the binding process by implementing a `BindAsync` method on a type. + +#### TryParse + +`TryParse` has two APIs: + +```csharp +public static bool TryParse(string value, T out result); +public static bool TryParse(string value, IFormatProvider provider, T out result); +``` + +The following code displays `Point: 12.3, 10.1` with the URI `/map?Point=12.3,10.1`: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_cb)] + +#### BindAsync + +`BindAsync` has the following APIs: + +```csharp +public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter); +public static ValueTask BindAsync(HttpContext context); +``` + +The following code displays `SortBy:xyz, SortDirection:Desc, CurrentPage:99` with the URI `/products?SortBy=xyz&SortDir=Desc&Page=99`: + +[!code-csharp[](~/fundamentals/minimal-apis/7.0-samples/WebMinAPIs/Program.cs?name=snippet_ba)] + + + +### Binding failures + +When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode. + +|Failure mode|Nullable Parameter Type|Binding Source|Status code| +|--|--|--|--| +|`{ParameterType}.TryParse` returns `false` |yes|route/query/header|400| +|`{ParameterType}.BindAsync` returns `null` |yes|custom|400| +|`{ParameterType}.BindAsync` throws |does not matter|custom|500| +| Failure to deserialize JSON body |does not matter|body|400| +| Wrong content type (not `application/json`) |does not matter|body|415| + +### Binding Precedence + +The rules for determining a binding source from a parameter: + +1. Explicit attribute defined on parameter (From* attributes) in the following order: + 1. Route values: [`[FromRoute]`](xref:Microsoft.AspNetCore.Mvc.FromRouteAttribute) + 1. Query string: [`[FromQuery]`](xref:Microsoft.AspNetCore.Mvc.FromQueryAttribute) + 1. Header: [`[FromHeader]`](xref:Microsoft.AspNetCore.Mvc.FromHeaderAttribute) + 1. Body: [`[FromBody]`](xref:Microsoft.AspNetCore.Mvc.FromBodyAttribute) + 1. Service: [`[FromServices]`](xref:Microsoft.AspNetCore.Mvc.FromServicesAttribute) + 1. Parameter values: [`[AsParameters]`](xref:Microsoft.AspNetCore.Http.AsParametersAttribute) +1. Special types + 1. [`HttpContext`](xref:Microsoft.AspNetCore.Http.HttpContext) + 1. [`HttpRequest`](xref:Microsoft.AspNetCore.Http.HttpRequest) ([`HttpContext.Request`](xref:Microsoft.AspNetCore.Http.HttpContext.Request)) + 1. [`HttpResponse`](xref:Microsoft.AspNetCore.Http.HttpResponse) ([`HttpContext.Response`](xref:Microsoft.AspNetCore.Http.HttpContext.Response)) + 1. [`ClaimsPrincipal`](xref:System.Security.Claims.ClaimsPrincipal) ([`HttpContext.User`](xref:Microsoft.AspNetCore.Http.HttpContext.User)) + 1. [`CancellationToken`](xref:System.Threading.CancellationToken) ([`HttpContext.RequestAborted`](xref:Microsoft.AspNetCore.Http.HttpContext.RequestAborted)) + 1. [`IFormFileCollection`](xref:Microsoft.AspNetCore.Http.IFormFileCollection) ([`HttpContext.Request.Form.Files`](xref:Microsoft.AspNetCore.Http.IFormCollection.Files)) + 1. [`IFormFile`](xref:Microsoft.AspNetCore.Http.IFormFile) ([`HttpContext.Request.Form.Files[paramName]`](xref:Microsoft.AspNetCore.Http.IFormFileCollection.Item(System.String))) + 1. [`Stream`](xref:System.IO.Stream) ([`HttpContext.Request.Body`](xref:Microsoft.AspNetCore.Http.HttpRequest.Body)) + 1. [`PipeReader`](xref:System.IO.Pipelines.PipeReader) ([`HttpContext.Request.BodyReader`](xref:Microsoft.AspNetCore.Http.HttpRequest.BodyReader)) +1. Parameter type has a valid static [`BindAsync`](xref:Microsoft.AspNetCore.Http.IBindableFromHttpContext%601.BindAsync%2A) method. +1. Parameter type is a string or has a valid static [`TryParse`](xref:System.IParsable%601.TryParse%2A) method. + 1. If the parameter name exists in the route template e.g. `app.Map("/todo/{id}", (int id) => {});`, then it's bound from the route. + 1. Bound from the query string. +1. If the parameter type is a service provided by dependency injection, it uses that service as the source. +1. The parameter is from the body. + +### Configure JSON deserialization options for body binding + +The body binding source uses for deserialization. It is ***not*** possible to change this default, but JSON serialization and deserialization options can be configured. + +#### Configure JSON deserialization options globally + +Options that apply globally for an app can be configured by invoking . The following example includes public fields and formats JSON output. + +:::code language="csharp" source="~/fundamentals/minimal-apis/7.0-samples/WebMinJson/Program.cs" id="snippet_confighttpjsonoptions" highlight="3-6"::: + +Since the sample code configures both serialization and deserialization, it can read `NameField` and include `NameField` in the output JSON. + +#### Configure JSON deserialization options for an endpoint + + has overloads that accept a object. The following example includes public fields and formats JSON output. + +:::code language="csharp" source="~/fundamentals/minimal-apis/7.0-samples/WebMinJson/Program.cs" id="snippet_readfromjsonasyncwithoptions" highlight="5-8,12"::: + +Since the preceding code applies the customized options only to deserialization, the output JSON excludes `NameField`. + +## Binding to forms with IFormCollection, IFormFile, and IFormFileCollection + +Binding from form-based parameters using , , and is supported. [OpenAPI](xref:fundamentals/minimal-apis/openapi) metadata is inferred for form parameters to support integration with [Swagger UI](xref:tutorials/web-api-help-pages-using-swagger). + +The following code uploads files using inferred binding from the `IFormFile` type: + +:::code language="csharp" source="~/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Program.cs" highlight="17-22,43-57"::: + +***Warning:*** When implementing forms, the app ***must prevent*** [Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery). In the preceding code, the service is used to prevent XSRF attacks by generating and validation an anti-forgery token: + +:::code language="csharp" source="~/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Program.cs" highlight="24,44"::: + +For more information on XSRF attacks, see [Cross-Site Request Forgery (XSRF/CSRF) attacks](xref:security/anti-request-forgery). + +:::moniker-end diff --git a/aspnetcore/fundamentals/minimal-apis/parameter-binding.md b/aspnetcore/fundamentals/minimal-apis/parameter-binding.md index 4986e10e4f..5a7bcd2e70 100644 --- a/aspnetcore/fundamentals/minimal-apis/parameter-binding.md +++ b/aspnetcore/fundamentals/minimal-apis/parameter-binding.md @@ -10,4 +10,5 @@ uid: fundamentals/minimal-apis/parameter-binding # Parameter Binding in Minimal API apps -[!INCLUDE [parameter-binding](includes/parameter-binding.md)] +[!INCLUDE [parameter-binding](~/fundamentals/minimal-apis/includes/parameter-binding8.md)] +[!INCLUDE [parameter-binding](~/fundamentals/minimal-apis/includes/parameter-binding7.md)] diff --git a/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Iform.csproj b/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Iform.csproj new file mode 100644 index 0000000000..1b28a01c81 --- /dev/null +++ b/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Iform.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Program.cs b/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Program.cs new file mode 100644 index 0000000000..58240c112c --- /dev/null +++ b/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/Program.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http.HttpResults; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddAntiforgery(); + +var app = builder.Build(); + +string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles") +{ + var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory); + Directory.CreateDirectory(directoryPath); + return Path.Combine(directoryPath, fileName); +} + +async Task UploadFileWithName(IFormFile file, string fileSaveName) +{ + var filePath = GetOrCreateFilePath(fileSaveName); + await using var fileStream = new FileStream(filePath, FileMode.Create); + await file.CopyToAsync(fileStream); +} + +app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) => +{ + var token = antiforgery.GetAndStoreTokens(context); + var html = $""" + + +
+ + + +
+ + + """; + + return Results.Content(html, "text/html"); +}); + +app.MapPost("/upload", async Task, + BadRequest>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) => +{ + try + { + await antiforgery.ValidateRequestAsync(context); + var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName); + await UploadFileWithName(file, fileSaveName); + return TypedResults.Ok("File uploaded successfully!"); + } + catch (AntiforgeryValidationException e) + { + return TypedResults.BadRequest("Invalid anti-forgery token"); + } +}); + +app.Run(); diff --git a/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/appsettings.Development.json b/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/aspnetcore/fundamentals/minimal-apis/parameter-binding/samples8/Iform/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore/release-notes/aspnetcore-8.0.md b/aspnetcore/release-notes/aspnetcore-8.0.md index 2078474fdb..2b729a630c 100644 --- a/aspnetcore/release-notes/aspnetcore-8.0.md +++ b/aspnetcore/release-notes/aspnetcore-8.0.md @@ -19,6 +19,14 @@ This article is under development and not complete. More information may be foun ## Blazor +## Minimal APIs + +### Binding to forms with IFormCollection, IFormFile, and IFormFileCollection + +Binding tp forms using , , and is now supported. [OpenAPI](xref:fundamentals/minimal-apis/openapi) metadata is inferred for form parameters to support integration with [Swagger UI](xref:tutorials/web-api-help-pages-using-swagger). + +For more information, see [Binding to forms with IFormCollection, IFormFile, and IFormFileCollection](xref:fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0&preserve-view=true#binding-to-forms-with-iformcollection-iformfile-and-iformfilecollection) + ## Miscellaneous ### Support for native AOT