Skip to content

Commit 563ef9c

Browse files
authored
Update minimal APIs form binding integration (#48999)
1 parent 2cef6d8 commit 563ef9c

File tree

2 files changed

+67
-11
lines changed

2 files changed

+67
-11
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.ObjectModel;
4+
using System.Buffers;
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Globalization;
@@ -116,9 +116,11 @@ public static partial class RequestDelegateFactory
116116
private static readonly MemberExpression FilterContextHttpContextStatusCodeExpr = Expression.Property(FilterContextHttpContextResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
117117
private static readonly ParameterExpression InvokedFilterContextExpr = Expression.Parameter(typeof(EndpointFilterInvocationContext), "filterContext");
118118

119-
private static readonly ConstructorInfo FormDataReaderConstructor = typeof(FormDataReader).GetConstructor(new[] { typeof(IReadOnlyDictionary<string, StringValues>), typeof(CultureInfo) })!;
120-
private static readonly MethodInfo FormToReadOnlyDictionaryMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ToReadOnlyDictionary), BindingFlags.Static | BindingFlags.NonPublic, new[] { typeof(IFormCollection) })!;
119+
private static readonly ConstructorInfo FormDataReaderConstructor = typeof(FormDataReader).GetConstructor(new[] { typeof(IReadOnlyDictionary<FormKey, StringValues>), typeof(CultureInfo), typeof(Memory<char>) })!;
120+
private static readonly MethodInfo ProcessFormMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ProcessForm), BindingFlags.Static | BindingFlags.NonPublic)!;
121121
private static readonly MethodInfo FormDataMapperMapMethod = typeof(FormDataMapper).GetMethod(nameof(FormDataMapper.Map))!;
122+
private static readonly MethodInfo AsMemoryMethod = new Func<char[]?, int, int, Memory<char>>(MemoryExtensions.AsMemory).Method;
123+
private static readonly MethodInfo ArrayPoolSharedReturnMethod = typeof(ArrayPool<char>).GetMethod(nameof(ArrayPool<char>.Shared.Return))!;
122124

123125
private static readonly string[] DefaultAcceptsAndProducesContentType = new[] { JsonConstants.JsonContentType };
124126
private static readonly string[] FormFileContentType = new[] { "multipart/form-data" };
@@ -738,7 +740,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
738740
return BindParameterFromFormCollection(parameter, factoryContext);
739741
}
740742
// Continue to use the simple binding support that exists in RDF/RDG for currently
741-
// supported scenarios to maintain compatible semantics between versions of RDG.
743+
// supported scenarios to maintain compatible semantics between versions of RDG.
742744
// For complex types, leverage the shared form binding infrastructure. For example,
743745
// shared form binding does not currently only supports types that implement IParsable
744746
// while RDF's binding implementation supports all TryParse implementations.
@@ -1972,30 +1974,56 @@ private static Expression BindComplexParameterFromFormItem(
19721974

19731975
// var name_local;
19741976
// var name_reader;
1977+
// var form_dict;
1978+
// var form_buffer;
1979+
// var form_max_key_length;
19751980
var formArgument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
19761981
var formReader = Expression.Variable(typeof(FormDataReader), $"{parameter.Name}_reader");
1982+
var formDict = Expression.Variable(typeof(IReadOnlyDictionary<FormKey, StringValues>), "form_dict");
1983+
var formBuffer = Expression.Variable(typeof(char[]), "form_buffer");
1984+
var formMaxKeyLength = Expression.Variable(typeof(int), "form_max_key_length");
19771985

1978-
// name_reader = new FormDataReader(context.Request.Form.ToReadOnlyDictionary()), CultureInfo.InvariantCulture);
1986+
// ProcessForm(context.Request.Form, form_dict, form_max_key_length, form_buffer);
1987+
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, formDict, formMaxKeyLength, formBuffer);
1988+
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, form_max_key_length));
19791989
var initializeReaderExpr = Expression.Assign(
19801990
formReader,
19811991
Expression.New(FormDataReaderConstructor,
1982-
Expression.Call(FormToReadOnlyDictionaryMethod, FormExpr),
1983-
Expression.Constant(CultureInfo.InvariantCulture)));
1992+
formDict,
1993+
Expression.Constant(CultureInfo.InvariantCulture),
1994+
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), formMaxKeyLength)));
19841995
// FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
19851996
var invokeMapMethodExpr = Expression.Call(
19861997
FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
19871998
formReader,
19881999
Expression.Constant(FormDataMapperOptions));
2000+
// ArrayPool<char>.Shared.Return(form_buffer);
2001+
var returnBufferExpr = Expression.Call(ArrayPoolSharedReturnMethod, formBuffer);
19892002

19902003
return Expression.Block(
1991-
new[] { formArgument, formReader },
2004+
new[] { formArgument, formReader, formDict, formBuffer },
2005+
processFormExpr,
19922006
initializeReaderExpr,
1993-
Expression.Assign(formArgument, invokeMapMethodExpr)
2007+
Expression.Assign(formArgument, invokeMapMethodExpr),
2008+
returnBufferExpr
19942009
);
19952010
}
19962011

1997-
private static IReadOnlyDictionary<string, StringValues> ToReadOnlyDictionary(IFormCollection form)
1998-
=> new ReadOnlyDictionary<string, StringValues>(form.ToDictionary());
2012+
private static void ProcessForm(IFormCollection form, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref int maxKeyLength, ref char[] buffer)
2013+
{
2014+
var dictionary = new Dictionary<FormKey, StringValues>();
2015+
maxKeyLength = -1;
2016+
foreach (var (key, value) in form)
2017+
{
2018+
if (key.Length > maxKeyLength)
2019+
{
2020+
maxKeyLength = key.Length;
2021+
}
2022+
dictionary.Add(new FormKey(key.AsMemory()), value);
2023+
}
2024+
formDictionary = dictionary.AsReadOnly();
2025+
buffer = ArrayPool<char>.Shared.Rent(maxKeyLength);
2026+
}
19992027

20002028
private static Expression BindParameterFromFormFiles(
20012029
ParameterInfo parameter,

src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.ComplexFormBinding.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Net.Http;
5+
using Microsoft.AspNetCore.Components.Endpoints.Binding;
56
using Microsoft.AspNetCore.Http.Features;
67
using Microsoft.Extensions.Primitives;
78

@@ -139,6 +140,33 @@ await VerifyResponseJsonBodyAsync<Dictionary<string, bool>>(httpContext, (elemen
139140
});
140141
}
141142

143+
[Fact]
144+
public async Task SupportsBindingInvalidDictionaryFromForm_Multipart()
145+
{
146+
var source = """
147+
app.MapPost("/", ([FromForm] Dictionary<string, bool> elements) => Results.Ok(elements));
148+
""";
149+
var (_, compilation) = await RunGeneratorAsync(source);
150+
var endpoint = GetEndpointFromCompilation(compilation);
151+
var httpContext = CreateHttpContext();
152+
153+
var content = new MultipartFormDataContent("some-boundary");
154+
content.Add(new StringContent("not-a-bool"), "[foo]");
155+
content.Add(new StringContent("1"), "[bar]");
156+
content.Add(new StringContent("2"), "[baz]");
157+
158+
var stream = new MemoryStream();
159+
await content.CopyToAsync(stream);
160+
161+
stream.Seek(0, SeekOrigin.Begin);
162+
163+
httpContext.Request.Body = stream;
164+
httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
165+
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
166+
167+
await Assert.ThrowsAsync<FormDataMappingException>(async () => await endpoint.RequestDelegate(httpContext));
168+
}
169+
142170
[Fact]
143171
public async Task SupportsBindingListFromForm_UrlEncoded()
144172
{

0 commit comments

Comments
 (0)