Skip to content

[WIP] avoid form read before reaching endpoint handler code #62139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
585 changes: 293 additions & 292 deletions AspNetCore.slnx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Http/HttpAbstractions.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
117 changes: 117 additions & 0 deletions src/Http/samples/FileManagerSample/FileController.cs
Original file line number Diff line number Diff line change
@@ -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<IActionResult> 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<IFormFeature>();
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<IActionResult> 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<IFormFeature>();
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.");
}
}
26 changes: 26 additions & 0 deletions src/Http/samples/FileManagerSample/FileManagerSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Results" />
<!-- Mvc.Core is referenced only for its attributes -->
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj" OutputItemType="Analyzer" />
</ItemGroup>

</Project>
73 changes: 73 additions & 0 deletions src/Http/samples/FileManagerSample/Program.cs
Original file line number Diff line number Diff line change
@@ -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<FormOptions>(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();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"profiles": {
"HttpApiSampleApp": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
10 changes: 10 additions & 0 deletions src/Http/samples/FileManagerSample/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
16 changes: 15 additions & 1 deletion src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<string, StringValues>(),
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;

Expand Down
Loading