Skip to content

Commit 7065262

Browse files
authored
Resolve FormOptionsMetadata to FormOptions when reading form (#50166)
* Resolve FormOptionsMetadata to FormOptions when reading form * React to peer review * Pass FormOptionsMetadata as ref struct
1 parent ff097c6 commit 7065262

13 files changed

+117
-146
lines changed

src/Http/Http/src/Features/FormFeature.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.Text;
66
using Microsoft.AspNetCore.Antiforgery;
7+
using Microsoft.AspNetCore.Http.Metadata;
78
using Microsoft.AspNetCore.WebUtilities;
89
using Microsoft.Extensions.Primitives;
910
using Microsoft.Net.Http.Headers;
@@ -16,7 +17,8 @@ namespace Microsoft.AspNetCore.Http.Features;
1617
public class FormFeature : IFormFeature
1718
{
1819
private readonly HttpRequest _request;
19-
private readonly FormOptions _options;
20+
private readonly Endpoint? _endpoint;
21+
private FormOptions _options;
2022
private Task<IFormCollection>? _parsedFormTask;
2123
private IFormCollection? _form;
2224

@@ -48,12 +50,18 @@ public FormFeature(HttpRequest request)
4850
/// <param name="request">The <see cref="HttpRequest"/>.</param>
4951
/// <param name="options">The <see cref="FormOptions"/>.</param>
5052
public FormFeature(HttpRequest request, FormOptions options)
53+
: this(request, options, null)
54+
{
55+
}
56+
57+
internal FormFeature(HttpRequest request, FormOptions options, Endpoint? endpoint)
5158
{
5259
ArgumentNullException.ThrowIfNull(request);
5360
ArgumentNullException.ThrowIfNull(options);
5461

5562
_request = request;
5663
_options = options;
64+
_endpoint = endpoint;
5765
}
5866

5967
// Internal for testing.
@@ -144,6 +152,7 @@ public Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
144152
private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancellationToken)
145153
{
146154
HandleUncheckedAntiforgeryValidationFeature();
155+
_options = _endpoint is null ? _options : GetFormOptionsFromMetadata(_options, _endpoint);
147156

148157
if (!HasFormContentType)
149158
{
@@ -345,4 +354,21 @@ private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLi
345354
}
346355
return boundary.ToString();
347356
}
357+
358+
private static FormOptions GetFormOptionsFromMetadata(FormOptions baseFormOptions, Endpoint endpoint)
359+
{
360+
var formOptionsMetadatas = endpoint.Metadata
361+
.GetOrderedMetadata<IFormOptionsMetadata>();
362+
var metadataCount = formOptionsMetadatas.Count;
363+
if (metadataCount == 0)
364+
{
365+
return baseFormOptions;
366+
}
367+
var finalFormOptionsMetadata = new MutableFormOptionsMetadata(formOptionsMetadatas[metadataCount - 1]);
368+
for (int i = metadataCount - 2; i >= 0; i--)
369+
{
370+
formOptionsMetadatas[i].MergeWith(ref finalFormOptionsMetadata);
371+
}
372+
return finalFormOptionsMetadata.ResolveFormOptions(baseFormOptions);
373+
}
348374
}

src/Http/Http/src/Internal/DefaultHttpRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal sealed class DefaultHttpRequest : HttpRequest
1515
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
1616
private static readonly Func<IFeatureCollection, IHttpRequestFeature?> _nullRequestFeature = f => null;
1717
private static readonly Func<IFeatureCollection, IQueryFeature?> _newQueryFeature = f => new QueryFeature(f);
18-
private static readonly Func<DefaultHttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default);
18+
private static readonly Func<DefaultHttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default, r._context.GetEndpoint());
1919
private static readonly Func<IFeatureCollection, IRequestCookiesFeature> _newRequestCookiesFeature = f => new RequestCookiesFeature(f);
2020
private static readonly Func<IFeatureCollection, IRouteValuesFeature> _newRouteValuesFeature = f => new RouteValuesFeature();
2121
private static readonly Func<HttpContext, IRequestBodyPipeFeature> _newRequestBodyPipeFeature = context => new RequestBodyPipeFeature(context);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
6+
namespace Microsoft.AspNetCore.Http.Features;
7+
8+
internal static class FormOptionsMetadataExtensions
9+
{
10+
public static void MergeWith(
11+
this IFormOptionsMetadata formOptionsMetadata,
12+
ref MutableFormOptionsMetadata otherFormOptionsMetadata)
13+
{
14+
otherFormOptionsMetadata.BufferBody ??= formOptionsMetadata.BufferBody;
15+
otherFormOptionsMetadata.MemoryBufferThreshold ??= formOptionsMetadata.MemoryBufferThreshold;
16+
otherFormOptionsMetadata.BufferBodyLengthLimit ??= formOptionsMetadata.BufferBodyLengthLimit;
17+
otherFormOptionsMetadata.ValueCountLimit ??= formOptionsMetadata.ValueCountLimit;
18+
otherFormOptionsMetadata.KeyLengthLimit ??= formOptionsMetadata.KeyLengthLimit;
19+
otherFormOptionsMetadata.ValueLengthLimit ??= formOptionsMetadata.ValueLengthLimit;
20+
otherFormOptionsMetadata.MultipartBoundaryLengthLimit ??= formOptionsMetadata.MultipartBoundaryLengthLimit;
21+
otherFormOptionsMetadata.MultipartHeadersCountLimit ??= formOptionsMetadata.MultipartHeadersCountLimit;
22+
otherFormOptionsMetadata.MultipartHeadersLengthLimit ??= formOptionsMetadata.MultipartHeadersLengthLimit;
23+
otherFormOptionsMetadata.MultipartBodyLengthLimit ??= formOptionsMetadata.MultipartBodyLengthLimit;
24+
}
25+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
using Microsoft.AspNetCore.Http.Metadata;
4+
5+
namespace Microsoft.AspNetCore.Http.Features;
6+
7+
internal struct MutableFormOptionsMetadata(IFormOptionsMetadata formOptionsMetadata) : IFormOptionsMetadata
8+
{
9+
internal FormOptions ResolveFormOptions(FormOptions baseFormOptions) => new FormOptions
10+
{
11+
BufferBody = BufferBody ?? baseFormOptions.BufferBody,
12+
MemoryBufferThreshold = MemoryBufferThreshold ?? baseFormOptions.MemoryBufferThreshold,
13+
BufferBodyLengthLimit = BufferBodyLengthLimit ?? baseFormOptions.BufferBodyLengthLimit,
14+
ValueCountLimit = ValueCountLimit ?? baseFormOptions.ValueCountLimit,
15+
KeyLengthLimit = KeyLengthLimit ?? baseFormOptions.KeyLengthLimit,
16+
ValueLengthLimit = ValueLengthLimit ?? baseFormOptions.ValueLengthLimit,
17+
MultipartBoundaryLengthLimit = MultipartBoundaryLengthLimit ?? baseFormOptions.MultipartBoundaryLengthLimit,
18+
MultipartHeadersCountLimit = MultipartHeadersCountLimit ?? baseFormOptions.MultipartHeadersCountLimit,
19+
MultipartHeadersLengthLimit = MultipartHeadersLengthLimit ?? baseFormOptions.MultipartHeadersLengthLimit,
20+
MultipartBodyLengthLimit = MultipartBodyLengthLimit ?? baseFormOptions.MultipartBodyLengthLimit
21+
};
22+
23+
public bool? BufferBody { get; set; } = formOptionsMetadata.BufferBody;
24+
public int? MemoryBufferThreshold { get; set; } = formOptionsMetadata.MemoryBufferThreshold;
25+
public long? BufferBodyLengthLimit { get; set; } = formOptionsMetadata.BufferBodyLengthLimit;
26+
public int? ValueCountLimit { get; set; } = formOptionsMetadata.ValueCountLimit;
27+
public int? KeyLengthLimit { get; set; } = formOptionsMetadata.KeyLengthLimit;
28+
public int? ValueLengthLimit { get; set; } = formOptionsMetadata.ValueLengthLimit;
29+
public int? MultipartBoundaryLengthLimit { get; set; } = formOptionsMetadata.MultipartBoundaryLengthLimit;
30+
public int? MultipartHeadersCountLimit { get; set; } = formOptionsMetadata.MultipartHeadersCountLimit;
31+
public int? MultipartHeadersLengthLimit { get; set; } = formOptionsMetadata.MultipartHeadersLengthLimit;
32+
public long? MultipartBodyLengthLimit { get; set; } = formOptionsMetadata.MultipartBodyLengthLimit;
33+
}

src/Http/Http/src/Microsoft.AspNetCore.Http.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15-
<Compile Include="$(SharedSourceRoot)CancellationTokenSourcePool.cs" />
16-
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" LinkBase="Shared"/>
17-
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared"/>
15+
<Compile Include="$(SharedSourceRoot)CancellationTokenSourcePool.cs" />
16+
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" LinkBase="Shared" />
17+
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared" />
1818
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" Link="Internal\StreamCopyOperationInternal.cs" />
1919
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="Internal\CookieHeaderParserShared.cs" />
2020
<Compile Include="$(SharedSourceRoot)HttpRuleParser.cs" LinkBase="Shared" />

src/Http/Routing/perf/Microbenchmarks/EndpointRoutingShortCircuitBenchmark.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public void Setup()
3333
new BenchmarkEndpointDataSource(),
3434
new DiagnosticListener("benchmark"),
3535
Options.Create(new RouteOptions()),
36-
Options.Create(new FormOptions()),
3736
routingMetrics,
3837
context => Task.CompletedTask);
3938

@@ -46,7 +45,6 @@ public void Setup()
4645
new BenchmarkEndpointDataSource(),
4746
new DiagnosticListener("benchmark"),
4847
Options.Create(new RouteOptions()),
49-
Options.Create(new FormOptions()),
5048
routingMetrics,
5149
context => Task.CompletedTask);
5250
}

src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public static TBuilder WithOrder<TBuilder>(this TBuilder builder, int order) whe
145145
{
146146
throw new InvalidOperationException("This endpoint does not support Order.");
147147
}
148-
});
148+
});
149149
return builder;
150150
}
151151

src/Http/Routing/src/EndpointRoutingMiddleware.cs

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ internal sealed partial class EndpointRoutingMiddleware
2929
private readonly RoutingMetrics _metrics;
3030
private readonly RequestDelegate _next;
3131
private readonly RouteOptions _routeOptions;
32-
private readonly FormOptions _formOptions;
3332
private Task<Matcher>? _initializationTask;
3433

3534
public EndpointRoutingMiddleware(
@@ -39,7 +38,6 @@ public EndpointRoutingMiddleware(
3938
EndpointDataSource rootCompositeEndpointDataSource,
4039
DiagnosticListener diagnosticListener,
4140
IOptions<RouteOptions> routeOptions,
42-
IOptions<FormOptions> formOptions,
4341
RoutingMetrics metrics,
4442
RequestDelegate next)
4543
{
@@ -51,7 +49,6 @@ public EndpointRoutingMiddleware(
5149
_metrics = metrics;
5250
_next = next ?? throw new ArgumentNullException(nameof(next));
5351
_routeOptions = routeOptions.Value;
54-
_formOptions = formOptions.Value;
5552

5653
// rootCompositeEndpointDataSource is a constructor parameter only so it always gets disposed by DI. This ensures that any
5754
// disposable EndpointDataSources also get disposed. _endpointDataSource is a component of rootCompositeEndpointDataSource.
@@ -139,14 +136,6 @@ private Task SetRoutingAndContinue(HttpContext httpContext)
139136
// We do this during endpoint routing to ensure that successive middlewares in the pipeline
140137
// can access the feature with the correct value.
141138
SetMaxRequestBodySize(httpContext);
142-
// Map IFormOptionsMetadata to IFormFeature if present on the endpoint if the
143-
// request being processed is a form request. Note: we do a manual check for the
144-
// form content-types here to avoid prematurely accessing the form feature.
145-
if (HttpExtensions.IsValidHttpMethodForForm(httpContext.Request.Method) &&
146-
HttpExtensions.IsValidContentTypeForForm(httpContext.Request.ContentType))
147-
{
148-
SetFormOptions(httpContext, endpoint);
149-
}
150139

151140
var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
152141
if (shortCircuitMetadata is not null)
@@ -345,45 +334,6 @@ private void SetMaxRequestBodySize(HttpContext context)
345334
}
346335
}
347336

348-
private void SetFormOptions(HttpContext context, Endpoint endpoint)
349-
{
350-
var features = context.Features;
351-
var formFeature = features.Get<IFormFeature>();
352-
353-
// Request form has not been read yet, so set the limits
354-
if (formFeature == null || formFeature is { Form: null })
355-
{
356-
var baseFormOptions = _formOptions;
357-
var finalFormOptionsMetadata = new FormOptionsMetadata();
358-
var formOptionsMetadatas = endpoint.Metadata
359-
.GetOrderedMetadata<IFormOptionsMetadata>();
360-
foreach (var formOptionsMetadata in formOptionsMetadatas)
361-
{
362-
finalFormOptionsMetadata = finalFormOptionsMetadata.MergeWith(formOptionsMetadata);
363-
}
364-
365-
var formOptions = new FormOptions
366-
{
367-
BufferBody = finalFormOptionsMetadata.BufferBody ?? baseFormOptions.BufferBody,
368-
MemoryBufferThreshold = finalFormOptionsMetadata.MemoryBufferThreshold ?? baseFormOptions.MemoryBufferThreshold,
369-
BufferBodyLengthLimit = finalFormOptionsMetadata.BufferBodyLengthLimit ?? baseFormOptions.BufferBodyLengthLimit,
370-
ValueCountLimit = finalFormOptionsMetadata.ValueCountLimit ?? baseFormOptions.ValueCountLimit,
371-
KeyLengthLimit = finalFormOptionsMetadata.KeyLengthLimit ?? baseFormOptions.KeyLengthLimit,
372-
ValueLengthLimit = finalFormOptionsMetadata.ValueLengthLimit ?? baseFormOptions.ValueLengthLimit,
373-
MultipartBoundaryLengthLimit = finalFormOptionsMetadata.MultipartBoundaryLengthLimit ?? baseFormOptions.MultipartBoundaryLengthLimit,
374-
MultipartHeadersCountLimit = finalFormOptionsMetadata.MultipartHeadersCountLimit ?? baseFormOptions.MultipartHeadersCountLimit,
375-
MultipartHeadersLengthLimit = finalFormOptionsMetadata.MultipartHeadersLengthLimit ?? baseFormOptions.MultipartHeadersLengthLimit,
376-
MultipartBodyLengthLimit = finalFormOptionsMetadata.MultipartBodyLengthLimit ?? baseFormOptions.MultipartBodyLengthLimit
377-
};
378-
features.Set<IFormFeature>(new FormFeature(context.Request, formOptions));
379-
Log.AppliedFormOptions(_logger);
380-
}
381-
else
382-
{
383-
Log.CannotApplyFormOptions(_logger);
384-
}
385-
}
386-
387337
private static partial class Log
388338
{
389339
public static void MatchSuccess(ILogger logger, Endpoint endpoint)
@@ -427,11 +377,5 @@ public static void MatchSkipped(ILogger logger, Endpoint endpoint)
427377

428378
[LoggerMessage(12, LogLevel.Debug, "The maximum request body size has been disabled.", EventName = "MaxRequestBodySizeDisabled")]
429379
public static partial void MaxRequestBodySizeDisabled(ILogger logger);
430-
431-
[LoggerMessage(13, LogLevel.Warning, "Unable to apply configured form options since the request form has already been read.", EventName = "CannotApplyFormOptions")]
432-
public static partial void CannotApplyFormOptions(ILogger logger);
433-
434-
[LoggerMessage(14, LogLevel.Debug, "Applied the configured form options on the current request.", EventName = "AppliedFormOptions")]
435-
public static partial void AppliedFormOptions(ILogger logger);
436380
}
437381
}

src/Http/Routing/src/Internal/FormOptionsMetadataExtensions.cs

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)