-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Fixing minimal api header array binding #55803
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
Changes from all commits
c9cbe68
1b42dd4
b5a2e38
79bdc5a
6158c14
63dcea3
8e5f786
39b025b
b52fad2
f415785
aeb3354
5b31f50
1e3ec2e
b86969f
9c3f575
c78f1a9
99ec84b
9db0e6f
8bcad90
f81960c
7f8bca7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -276,7 +276,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat | |
var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; | ||
var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); | ||
var jsonSerializerOptions = serviceProvider.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; | ||
var formDataMapperOptions = new FormDataMapperOptions();; | ||
var formDataMapperOptions = new FormDataMapperOptions(); | ||
|
||
var factoryContext = new RequestDelegateFactoryContext | ||
{ | ||
|
@@ -758,7 +758,6 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat | |
{ | ||
throw new NotSupportedException( | ||
$"Assigning a value to the {nameof(IFromFormMetadata)}.{nameof(IFromFormMetadata.Name)} property is not supported for parameters of type {nameof(IFormCollection)}."); | ||
|
||
} | ||
return BindParameterFromFormCollection(parameter, factoryContext); | ||
} | ||
|
@@ -1628,6 +1627,7 @@ private static Expression BindParameterFromKeyedService(ParameterInfo parameter, | |
|
||
private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, RequestDelegateFactoryContext factoryContext, string source) | ||
{ | ||
|
||
if (parameter.ParameterType == typeof(string) || parameter.ParameterType == typeof(string[]) | ||
|| parameter.ParameterType == typeof(StringValues) || parameter.ParameterType == typeof(StringValues?)) | ||
{ | ||
|
@@ -1854,10 +1854,10 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres | |
} | ||
|
||
private static Expression BindParameterFromExpression( | ||
ParameterInfo parameter, | ||
MattyLeslie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Expression valueExpression, | ||
RequestDelegateFactoryContext factoryContext, | ||
string source) | ||
ParameterInfo parameter, | ||
Expression valueExpression, | ||
RequestDelegateFactoryContext factoryContext, | ||
string source) | ||
{ | ||
var nullability = factoryContext.NullabilityContext.Create(parameter); | ||
var isOptional = IsOptionalParameter(parameter, factoryContext); | ||
|
@@ -1868,15 +1868,27 @@ private static Expression BindParameterFromExpression( | |
var parameterNameConstant = Expression.Constant(parameter.Name); | ||
var sourceConstant = Expression.Constant(source); | ||
|
||
if (source == "header" && (parameter.ParameterType == typeof(string[]) || typeof(IEnumerable<string>).IsAssignableFrom(parameter.ParameterType))) | ||
{ | ||
var stringValuesExpr = Expression.Convert(valueExpression, typeof(StringValues)); | ||
var toStringArrayMethod = typeof(StringValues).GetMethod(nameof(StringValues.ToArray))!; | ||
var headerValuesArrayExpr = Expression.Call(stringValuesExpr, toStringArrayMethod); | ||
|
||
var splitAndTrimMethod = typeof(RequestDelegateFactory).GetMethod(nameof(SplitAndTrim), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!; | ||
var splitAndTrimExpr = Expression.Call(splitAndTrimMethod, headerValuesArrayExpr); | ||
|
||
valueExpression = Expression.Convert(splitAndTrimExpr, parameter.ParameterType); | ||
} | ||
|
||
if (!isOptional) | ||
{ | ||
// The following is produced if the parameter is required: | ||
// | ||
// argument = value["param1"]; | ||
MattyLeslie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// argument = value; | ||
// if (argument == null) | ||
// { | ||
// wasParamCheckFailure = true; | ||
// Log.RequiredParameterNotProvided(httpContext, "TypeOfValue", "param1"); | ||
// Log.RequiredParameterNotProvided(httpContext, "TypeOfValue", "parameterName", "source"); | ||
// } | ||
var checkRequiredStringParameterBlock = Expression.Block( | ||
Expression.Assign(argument, valueExpression), | ||
|
@@ -1890,8 +1902,6 @@ private static Expression BindParameterFromExpression( | |
) | ||
); | ||
|
||
// NOTE: when StringValues is used as a parameter, value["some_unpresent_parameter"] returns StringValue.Empty, and it's equivalent to (string?)null | ||
|
||
factoryContext.ExtraLocals.Add(argument); | ||
factoryContext.ParamCheckExpressions.Add(checkRequiredStringParameterBlock); | ||
return argument; | ||
|
@@ -1905,20 +1915,20 @@ private static Expression BindParameterFromExpression( | |
// when Nullable<StringValues> is used and the actual value is StringValues.Empty, we should pass in a Nullable<StringValues> | ||
return Expression.Block( | ||
Expression.Condition(Expression.Equal(valueExpression, Expression.Convert(Expression.Constant(StringValues.Empty), parameter.ParameterType)), | ||
Expression.Convert(Expression.Constant(null), parameter.ParameterType), | ||
valueExpression | ||
) | ||
); | ||
Expression.Convert(Expression.Constant(null), parameter.ParameterType), | ||
valueExpression | ||
) | ||
); | ||
} | ||
return valueExpression; | ||
} | ||
|
||
// The following is produced if the parameter is optional. Note that we convert the | ||
// default value to the target ParameterType to address scenarios where the user is | ||
// is setting null as the default value in a context where nullability is disabled. | ||
// setting null as the default value in a context where nullability is disabled. | ||
// | ||
// param1_local = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; | ||
// param1_local != null ? param1_local : Convert(null, Int32) | ||
// param1_local = value; | ||
// param1_local != null ? param1_local : Convert(defaultValue, ParameterType) | ||
return Expression.Block( | ||
Expression.Condition(Expression.NotEqual(valueExpression, Expression.Constant(null)), | ||
valueExpression, | ||
|
@@ -2846,6 +2856,39 @@ private static void FormatTrackedParameters(RequestDelegateFactoryContext factor | |
} | ||
} | ||
|
||
private static string[] SplitAndTrim(string[] values) | ||
{ | ||
if (values == null || values.Length == 0) | ||
{ | ||
return []; | ||
} | ||
|
||
var result = new List<string>(); | ||
MattyLeslie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Span<Range> parts = stackalloc Range[16]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem correct. If there are more than 16 values in a single header it will miss them. Maybe just revert to the previous code for now. We can look at improving the perf later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @BrennanConroy, I will revert that, I am going to mark this as draft as I'm still working on this. |
||
|
||
foreach (var value in values) | ||
{ | ||
if (string.IsNullOrWhiteSpace(value)) | ||
{ | ||
continue; | ||
} | ||
|
||
var valueSpan = value.AsSpan(); | ||
var length = valueSpan.Split(parts, ','); | ||
|
||
for (int i = 0; i < length; i++) | ||
{ | ||
var part = valueSpan[parts[i]].Trim(); | ||
if (!part.IsEmpty) | ||
{ | ||
result.Add(new string(part)); | ||
} | ||
} | ||
} | ||
|
||
return [.. result]; | ||
} | ||
|
||
private sealed class RdfEndpointBuilder : EndpointBuilder | ||
{ | ||
public RdfEndpointBuilder(IServiceProvider applicationServices) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When emitting code at compile-time, we want to do the discovery of parameters like this via static analysis.
You'll observe that in other parts of the Request Delegate Generator (RDG) code, we generate an
EndpointParameter
object that we construct after statically analyzing the arguments then we use the information in that data model to emit the generated code. We don't need to re-compute things like the headerName dynamically like this.You can find the pre-existing code where we emit the binding logic for header parameters here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the review, I will be working on this.