Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c9cbe68
Fixes
MattyLeslie May 15, 2024
1b42dd4
Merge branch 'dotnet:main' into fixingMinimalApiHeaderArrayBinding
MattyLeslie May 15, 2024
b5a2e38
Removing extra ';'
MattyLeslie May 15, 2024
79bdc5a
Merge branch 'dotnet:main' into fixingMinimalApiHeaderArrayBinding
MattyLeslie May 20, 2024
6158c14
Ensuring single header values and comma seperated header values are t…
MattyLeslie May 20, 2024
63dcea3
Test ammendments
MattyLeslie May 20, 2024
8e5f786
Adapting BindParameterFromExpression to handle nessisary cases
MattyLeslie May 20, 2024
39b025b
Merge branch 'fixingMinimalApiHeaderArrayBinding' of https://github.c…
MattyLeslie May 20, 2024
b52fad2
Removing trim method
MattyLeslie May 20, 2024
f415785
Adding comments
MattyLeslie May 20, 2024
aeb3354
Clean up
MattyLeslie May 20, 2024
5b31f50
Handling possible null occurances
MattyLeslie May 20, 2024
1e3ec2e
Removing extra lines
MattyLeslie May 20, 2024
b86969f
Merge branch 'dotnet:main' into fixingMinimalApiHeaderArrayBinding
MattyLeslie May 20, 2024
9c3f575
Improving performance and mimizing memory usage and allocations withi…
MattyLeslie May 20, 2024
c78f1a9
Update src/Http/Http.Extensions/src/RequestDelegateFactory.cs
MattyLeslie Jul 15, 2024
99ec84b
Update src/Http/Http.Extensions/src/RequestDelegateFactory.cs
MattyLeslie Jul 15, 2024
9db0e6f
Fixing indentations
MattyLeslie Jul 16, 2024
8bcad90
Fixing duplicate code and using MemoryAllocations.Split
MattyLeslie Jul 16, 2024
f81960c
Adding more test cases
MattyLeslie Jul 16, 2024
7f8bca7
Updating RequestDelegateGenerator
MattyLeslie Jul 16, 2024
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
103 changes: 77 additions & 26 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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?))
{
Expand Down Expand Up @@ -1854,10 +1854,10 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
}

private static Expression BindParameterFromExpression(
ParameterInfo parameter,
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);
Expand All @@ -1868,16 +1868,69 @@ private static Expression BindParameterFromExpression(
var parameterNameConstant = Expression.Constant(parameter.Name);
var sourceConstant = Expression.Constant(source);

if (source == "header" && (parameter.ParameterType.IsArray || typeof(IEnumerable<string>).IsAssignableFrom(parameter.ParameterType)))
{
var stringValuesExpr = Expression.Convert(valueExpression, typeof(StringValues));
var toStringArrayMethod = typeof(StringValues).GetMethod(nameof(StringValues.ToArray));
if (toStringArrayMethod == null)
{
throw new InvalidOperationException("The method 'ToArray' could not be found on 'StringValues'.");
}
var headerValuesArrayExpr = Expression.Call(stringValuesExpr, toStringArrayMethod);

var splitAndTrimMethod = typeof(RequestDelegateFactory).GetMethod(nameof(SplitAndTrim), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
if (splitAndTrimMethod == null)
{
throw new InvalidOperationException("The method 'SplitAndTrim' could not be found on 'RequestDelegateFactory'.");
}
var splitAndTrimExpr = Expression.Call(splitAndTrimMethod, headerValuesArrayExpr);

var boundValueExpr = Expression.Convert(splitAndTrimExpr, parameter.ParameterType);

if (!isOptional)
{
var checkRequiredStringParameterBlock = Expression.Block(
Expression.Assign(argument, boundValueExpr),
Expression.IfThen(Expression.Equal(argument, Expression.Constant(null)),
Expression.Block(
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
Expression.Call(LogRequiredParameterNotProvidedMethod,
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant,
Expression.Constant(factoryContext.ThrowOnBadRequest))
)
)
);

// 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;
}

// Allow nullable parameters that don't have a default value
if (nullability.ReadState != NullabilityState.NotNull && !parameter.HasDefaultValue)
{
if (parameter.ParameterType == typeof(StringValues?))
{
return Expression.Block(
Expression.Condition(Expression.Equal(boundValueExpr, Expression.Convert(Expression.Constant(StringValues.Empty), parameter.ParameterType)),
Expression.Convert(Expression.Constant(null), parameter.ParameterType),
boundValueExpr
)
);
}
return boundValueExpr;
}

return Expression.Block(
Expression.Condition(Expression.NotEqual(boundValueExpr, Expression.Constant(null)),
boundValueExpr,
Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType)));
}

if (!isOptional)
{
// The following is produced if the parameter is required:
//
// argument = value["param1"];
// if (argument == null)
// {
// wasParamCheckFailure = true;
// Log.RequiredParameterNotProvided(httpContext, "TypeOfValue", "param1");
// }
// The following is produced if the parameter is optional.
var checkRequiredStringParameterBlock = Expression.Block(
Expression.Assign(argument, valueExpression),
Expression.IfThen(Expression.Equal(argument, Expression.Constant(null)),
Expand All @@ -1890,8 +1943,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;
Expand All @@ -1905,20 +1956,14 @@ 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.
//
// param1_local = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"];
// param1_local != null ? param1_local : Convert(null, Int32)
return Expression.Block(
Expression.Condition(Expression.NotEqual(valueExpression, Expression.Constant(null)),
valueExpression,
Expand Down Expand Up @@ -2846,6 +2891,12 @@ private static void FormatTrackedParameters(RequestDelegateFactoryContext factor
}
}

private static string[] SplitAndTrim(string[] values)
{
var result = values.SelectMany(v => v.Split(',').Select(s => s.Trim())).ToArray();
return result;
}

private sealed class RdfEndpointBuilder : EndpointBuilder
{
public RdfEndpointBuilder(IServiceProvider applicationServices)
Expand Down
46 changes: 46 additions & 0 deletions src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3150,6 +3150,52 @@ static void TestAction([AsParameters] ParameterListRequiredNullableStringFromDif
Assert.Null(httpContext.Items["RequiredHeaderParam"]);
}

[Fact]
public async Task RequestDelegate_ShouldHandleSingleHeaderValue()
{
// Arrange
var httpContext = CreateHttpContext();
httpContext.Request.Headers["TestHeader"] = "HeaderValue";

var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, [FromHeader(Name = "TestHeader")] string headerValue) =>
{
httpContext.Items["headerValue"] = headerValue;
});

var requestDelegate = resultFactory.RequestDelegate;

// Act
await requestDelegate(httpContext);

// Assert
Assert.True(httpContext.Items.ContainsKey("headerValue"));
Assert.Equal("HeaderValue", httpContext.Items["headerValue"]);
}

[Fact]
public async Task RequestDelegate_ShouldHandleCommaSeparatedHeaderValues()
{
// Arrange
var httpContext = CreateHttpContext();
httpContext.Request.Headers["TestHeader"] = "Value1,Value2,Value3";

var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, [FromHeader(Name = "TestHeader")] string[] headerValues) =>
{
httpContext.Items["headerValues"] = headerValues;
});

var requestDelegate = resultFactory.RequestDelegate;

// Act
await requestDelegate(httpContext);

// Assert
Assert.True(httpContext.Items.ContainsKey("headerValues"));
var headerValues = httpContext.Items["headerValues"] as string[];
Assert.NotNull(headerValues);
Assert.Equal(new[] { "Value1", "Value2", "Value3" }, headerValues);
}

#nullable disable
private class ParameterListMixedRequiredStringsFromDifferentSources
{
Expand Down