diff --git a/AspNetCore.slnx b/AspNetCore.slnx index 78284eb90b2c..d37e0b22a77e 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -6,95 +6,95 @@ - + - + - + - - + + - + - - + + - + - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + @@ -106,31 +106,31 @@ - + - - + + - + - + - - + + - + - + @@ -141,102 +141,102 @@ - + - + - + - - + + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - - + + @@ -244,171 +244,172 @@ - - + + - + - + - - + + - + - + - + - + - + - + - + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - - + + @@ -434,57 +435,57 @@ - - + + - + - + - - + + - + - + - + - + - + - + - + - + - + - + @@ -492,224 +493,224 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -736,120 +737,120 @@ - + - - + + - + - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -859,29 +860,29 @@ - + - - + + - + - + - + - - + + @@ -921,7 +922,7 @@ - + @@ -931,7 +932,7 @@ - + @@ -940,7 +941,7 @@ - + @@ -949,7 +950,7 @@ - + @@ -978,7 +979,7 @@ - + @@ -988,33 +989,33 @@ - + - + - + - - + + - + - + - + @@ -1024,84 +1025,84 @@ - + - + - + - + - + - - + + - + - - - - + + + + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + @@ -1109,71 +1110,71 @@ - - + + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 69a4e98a5599..3f65797ff3f9 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -47,6 +47,7 @@ "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", + "src\\Http\\samples\\FileManagerSample\\FileManagerSample.csproj", "src\\Http\\samples\\MinimalSampleOwin\\MinimalSampleOwin.csproj", "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", diff --git a/src/Http/samples/FileManagerSample/FileController.cs b/src/Http/samples/FileManagerSample/FileController.cs new file mode 100644 index 000000000000..728ad3430e38 --- /dev/null +++ b/src/Http/samples/FileManagerSample/FileController.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; + +namespace FileManagerSample; + +[ApiController] +[Route("controller")] +public class FileController : ControllerBase +{ + // curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/controller/upload + [HttpPost] + [Route("upload")] + public async Task Upload() + { + // 1. endpoint handler + // 2. form feature initialization + // 3. calling `Request.Form.Files.First()` + // 4. calling `FormFeature.InnerReadFormAsync()` + + if (!Request.HasFormContentType) + { + return BadRequest("The request does not contain a valid form."); + } + + // calling ReadFormAsync allows to await for form read (not blocking file read, opposite to `Request.Form.Files.`) + var formFeature = Request.HttpContext.Features.GetRequiredFeature(); + await formFeature.ReadFormAsync(HttpContext.RequestAborted); + + var file = Request.Form.Files.First(); + return Ok($"File '{file.Name}' uploaded."); + } + + // curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/controller/upload-cts + [HttpPost] + [Route("upload-cts")] + public async Task UploadCts(CancellationToken cancellationToken) + { + // 1. form feature initialization + // 2.calling `FormFeature.InnerReadFormAsync()` + // 3. endpoint handler + // 4. calling `Request.Form.Files.First()` + + if (!Request.HasFormContentType) + { + return BadRequest("The request does not contain a valid form."); + } + + var formFeature = Request.HttpContext.Features.GetRequiredFeature(); + await formFeature.ReadFormAsync(cancellationToken); + + var file = Request.Form.Files.First(); + return Ok($"File '{file.Name}' uploaded."); + } + + // curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/controller/upload-cts-noop + [HttpPost] + [Route("upload-cts-noop")] + public IActionResult UploadCtsNoop(CancellationToken cancellationToken) + { + // 1. form feature initialization + // 2.calling `FormFeature.InnerReadFormAsync()` + // 3. endpoint handler + + return Ok($"Noop completed."); + } + + // curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/controller/upload-str-noop + [HttpPost] + [Route("upload-str-noop")] + public IActionResult UploadStrNoop(string str) + { + // 1. form feature initialization + // 2.calling `FormFeature.InnerReadFormAsync()` + // 3. endpoint handler + + return Ok($"Str completed."); + } + + // curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/controller/upload-str-query + [HttpPost] + [Route("upload-str-query")] + public IActionResult UploadStrQuery([FromQuery] string str) + { + // 1. form feature initialization + // 2.calling `FormFeature.InnerReadFormAsync()` + // 3. endpoint handler + + return Ok($"Query completed."); + } + + // curl -X POST -F "file=@D:\.other\big-files\bigfile_50mb.dat" http://localhost:5000/controller/upload-route-param/1 + [HttpPost] + [Route("upload-route-param/{id}")] + public IActionResult UploadQueryParam(string id) + { + // 1. form feature initialization + // 2.calling `FormFeature.InnerReadFormAsync()` + // 3. endpoint handler + + return Ok($"Query completed: query id = {id}"); + } + + // curl -X POST -F "file=@D:\.other\big-files\bigfile_50mb.dat" http://localhost:5000/controller/upload-file + [HttpPost] + [Route("upload-file")] + public IActionResult UploadForm([FromForm] IFormFile file) + { + // 1. form feature initialization + // 2.calling `FormFeature.InnerReadFormAsync()` + // 3. endpoint handler + + return Ok($"File '{file.FileName}' uploaded."); + } +} diff --git a/src/Http/samples/FileManagerSample/FileManagerSample.csproj b/src/Http/samples/FileManagerSample/FileManagerSample.csproj new file mode 100644 index 000000000000..a4e10ed6b75b --- /dev/null +++ b/src/Http/samples/FileManagerSample/FileManagerSample.csproj @@ -0,0 +1,26 @@ + + + + $(DefaultNetCoreTargetFramework) + enable + true + true + true + + + + + + + + + + + + + + + + + + diff --git a/src/Http/samples/FileManagerSample/Program.cs b/src/Http/samples/FileManagerSample/Program.cs new file mode 100644 index 000000000000..0a28e59fdf46 --- /dev/null +++ b/src/Http/samples/FileManagerSample/Program.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.ConfigureKestrel(options => +{ + // if not present, will throw similar exception: + // Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large. The max request body size is 30000000 bytes. + options.Limits.MaxRequestBodySize = 6L * 1024 * 1024 * 1024; // 6 GB + + // optional: timeout settings + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10); +}); + +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = 10L * 1024 * 1024 * 1024; // 10 GB +}); + +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +builder.Services.AddControllers(); +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + +var app = builder.Build(); +app.Logger.LogInformation($"Current process ID: {Environment.ProcessId}"); + +app.MapGet("/plaintext", () => "Hello, World!"); + +// curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/upload-cts +app.MapPost("/upload-cts", (HttpRequest request, CancellationToken cancellationToken) => +{ + // 1. endpoint handler + // 2. form feature initialization + // 3. calling `Request.Form.Files.First()` + // 4. calling `FormFeature.InnerReadFormAsync()` + + if (!request.HasFormContentType) + { + return Results.BadRequest("The request does not contain a valid form."); + } + + var file = request.Form.Files.First(); + return Results.Ok($"File '{file.Name}' uploaded."); +}); + +// curl -X POST -F "file=@D:\.other\big-files\bigfile.dat" http://localhost:5000/upload +app.MapPost("/upload", (HttpRequest request) => +{ + // 1. endpoint handler + // 2. form feature initialization + // 3. calling `Request.Form.Files.First()` + // 4. calling `FormFeature.InnerReadFormAsync()` + + if (!request.HasFormContentType) + { + return Results.BadRequest("The request does not contain a valid form."); + } + + var file = request.Form.Files.First(); + return Results.Ok($"File '{file.Name}' uploaded."); +}); + +app.MapControllers(); + +app.Run(); diff --git a/src/Http/samples/FileManagerSample/Properties/launchSettings.json b/src/Http/samples/FileManagerSample/Properties/launchSettings.json new file mode 100644 index 000000000000..e511a82777fb --- /dev/null +++ b/src/Http/samples/FileManagerSample/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "HttpApiSampleApp": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Http/samples/FileManagerSample/appsettings.Development.json b/src/Http/samples/FileManagerSample/appsettings.Development.json new file mode 100644 index 000000000000..8983e0fc1c5e --- /dev/null +++ b/src/Http/samples/FileManagerSample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Http/samples/FileManagerSample/appsettings.json b/src/Http/samples/FileManagerSample/appsettings.json new file mode 100644 index 000000000000..d9d9a9bff6fd --- /dev/null +++ b/src/Http/samples/FileManagerSample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs index cd62315ec604..a14046b4e799 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs @@ -22,14 +22,28 @@ public Task CreateValueProviderAsync(ValueProviderFactoryContext context) var request = context.ActionContext.HttpContext.Request; if (request.HasFormContentType) { + RegisterValueProvider(context); + // Allocating a Task only when the body is form data. - return AddValueProviderAsync(context); + //return AddValueProviderAsync(context); } return Task.CompletedTask; } + private static void RegisterValueProvider(ValueProviderFactoryContext context) + { + var valueProvider = new FormValueProvider( + BindingSource.Form, + new FormCollection(fields: null, files: null), + CultureInfo.CurrentCulture); + + context.ValueProviders.Add(valueProvider); + } + +#pragma warning disable IDE0051 // Remove unused private members private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) +#pragma warning restore IDE0051 // Remove unused private members { var request = context.ActionContext.HttpContext.Request; IFormCollection form; diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs index b5a234b3f443..98c1e430a842 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs @@ -6,6 +6,7 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ModelBinding; @@ -22,14 +23,28 @@ public Task CreateValueProviderAsync(ValueProviderFactoryContext context) var request = context.ActionContext.HttpContext.Request; if (request.HasFormContentType) { + RegisterValueProvider(context); + // Allocating a Task only when the body is form data. - return AddValueProviderAsync(context); + // return AddValueProviderAsync(context); } return Task.CompletedTask; } + private static void RegisterValueProvider(ValueProviderFactoryContext context) + { + var valueProvider = new JQueryFormValueProvider( + BindingSource.Form, + values: new Dictionary(), + CultureInfo.CurrentCulture); + + context.ValueProviders.Add(valueProvider); + } + +#pragma warning disable IDE0051 // Remove unused private members private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) +#pragma warning restore IDE0051 // Remove unused private members { var request = context.ActionContext.HttpContext.Request;