Skip to content

Update minimal APIs form binding integration #48999

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

Merged
merged 1 commit into from
Jun 26, 2023
Merged
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
50 changes: 39 additions & 11 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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

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

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

// var name_local;
// var name_reader;
// var form_dict;
// var form_buffer;
// var form_max_key_length;
var formArgument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
var formReader = Expression.Variable(typeof(FormDataReader), $"{parameter.Name}_reader");
var formDict = Expression.Variable(typeof(IReadOnlyDictionary<FormKey, StringValues>), "form_dict");
var formBuffer = Expression.Variable(typeof(char[]), "form_buffer");
var formMaxKeyLength = Expression.Variable(typeof(int), "form_max_key_length");

// name_reader = new FormDataReader(context.Request.Form.ToReadOnlyDictionary()), CultureInfo.InvariantCulture);
// ProcessForm(context.Request.Form, form_dict, form_max_key_length, form_buffer);
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, formDict, formMaxKeyLength, formBuffer);
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, form_max_key_length));
var initializeReaderExpr = Expression.Assign(
formReader,
Expression.New(FormDataReaderConstructor,
Expression.Call(FormToReadOnlyDictionaryMethod, FormExpr),
Expression.Constant(CultureInfo.InvariantCulture)));
formDict,
Expression.Constant(CultureInfo.InvariantCulture),
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), formMaxKeyLength)));
// FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
var invokeMapMethodExpr = Expression.Call(
FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
formReader,
Expression.Constant(FormDataMapperOptions));
// ArrayPool<char>.Shared.Return(form_buffer);
var returnBufferExpr = Expression.Call(ArrayPoolSharedReturnMethod, formBuffer);

return Expression.Block(
new[] { formArgument, formReader },
new[] { formArgument, formReader, formDict, formBuffer },
processFormExpr,
initializeReaderExpr,
Expression.Assign(formArgument, invokeMapMethodExpr)
Expression.Assign(formArgument, invokeMapMethodExpr),
returnBufferExpr
);
}

private static IReadOnlyDictionary<string, StringValues> ToReadOnlyDictionary(IFormCollection form)
=> new ReadOnlyDictionary<string, StringValues>(form.ToDictionary());
private static void ProcessForm(IFormCollection form, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref int maxKeyLength, ref char[] buffer)
{
var dictionary = new Dictionary<FormKey, StringValues>();
maxKeyLength = -1;
foreach (var (key, value) in form)
{
if (key.Length > maxKeyLength)
{
maxKeyLength = key.Length;
}
dictionary.Add(new FormKey(key.AsMemory()), value);
}
formDictionary = dictionary.AsReadOnly();
buffer = ArrayPool<char>.Shared.Rent(maxKeyLength);
}

private static Expression BindParameterFromFormFiles(
ParameterInfo parameter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http;
using Microsoft.AspNetCore.Components.Endpoints.Binding;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;

Expand Down Expand Up @@ -139,6 +140,33 @@ await VerifyResponseJsonBodyAsync<Dictionary<string, bool>>(httpContext, (elemen
});
}

[Fact]
public async Task SupportsBindingInvalidDictionaryFromForm_Multipart()
{
var source = """
app.MapPost("/", ([FromForm] Dictionary<string, bool> elements) => Results.Ok(elements));
""";
var (_, compilation) = await RunGeneratorAsync(source);
var endpoint = GetEndpointFromCompilation(compilation);
var httpContext = CreateHttpContext();

var content = new MultipartFormDataContent("some-boundary");
content.Add(new StringContent("not-a-bool"), "[foo]");
content.Add(new StringContent("1"), "[bar]");
content.Add(new StringContent("2"), "[baz]");

var stream = new MemoryStream();
await content.CopyToAsync(stream);

stream.Seek(0, SeekOrigin.Begin);

httpContext.Request.Body = stream;
httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));

await Assert.ThrowsAsync<FormDataMappingException>(async () => await endpoint.RequestDelegate(httpContext));
}

[Fact]
public async Task SupportsBindingListFromForm_UrlEncoded()
{
Expand Down