Skip to content

Commit f6c008b

Browse files
committed
Add RequestDelegateFactoryUtilities
1 parent 3c6ae93 commit f6c008b

7 files changed

+167
-92
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
<ItemGroup>
1414
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\**\*.cs" />
15-
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" Link="StreamCopyOperationInternal.cs" />
15+
<Compile Include="$(SharedSourceRoot)RequestDelegateFactoryUtilities.cs" />
16+
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" />
1617
</ItemGroup>
1718

1819
<ItemGroup>

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

Lines changed: 3 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Concurrent;
65
using System.Collections.Generic;
7-
using System.Globalization;
86
using System.IO;
97
using System.Linq;
108
using System.Linq.Expressions;
@@ -34,7 +32,6 @@ public static class RequestDelegateFactory
3432
private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
3533
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, string, Task>>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default));
3634
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
37-
private static readonly MethodInfo EnumTryParseMethod = GetEnumTryParseMethod();
3835
private static readonly MethodInfo LogParameterBindingFailureMethod = GetMethodInfo<Action<HttpContext, string, string, string>>((httpContext, parameterType, parameterName, sourceValue) =>
3936
Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue));
4037

@@ -56,8 +53,6 @@ public static class RequestDelegateFactory
5653

5754
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
5855

59-
private static readonly ConcurrentDictionary<Type, MethodInfo?> TryParseMethodCache = new();
60-
6156
/// <summary>
6257
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
6358
/// </summary>
@@ -191,7 +186,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
191186
{
192187
if (parameter.Name is null)
193188
{
194-
throw new InvalidOperationException("A parameter does not have a name! Was it genererated? All parameters must be named.");
189+
throw new InvalidOperationException("A parameter does not have a name! Was it generated? All parameters must be named.");
195190
}
196191

197192
var parameterCustomAttributes = parameter.GetCustomAttributes();
@@ -224,7 +219,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
224219
{
225220
return RequestAbortedExpr;
226221
}
227-
else if (parameter.ParameterType == typeof(string) || HasTryParseMethod(parameter))
222+
else if (parameter.ParameterType == typeof(string) || RequestDelegateFactoryUtilities.HasTryParseMethod(parameter))
228223
{
229224
return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext);
230225
}
@@ -462,72 +457,6 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
462457
};
463458
}
464459

465-
private static MethodInfo GetEnumTryParseMethod()
466-
{
467-
var staticEnumMethods = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static);
468-
469-
foreach (var method in staticEnumMethods)
470-
{
471-
if (!method.IsGenericMethod || method.Name != "TryParse" || method.ReturnType != typeof(bool))
472-
{
473-
continue;
474-
}
475-
476-
var tryParseParameters = method.GetParameters();
477-
478-
if (tryParseParameters.Length == 2 &&
479-
tryParseParameters[0].ParameterType == typeof(string) &&
480-
tryParseParameters[1].IsOut)
481-
{
482-
return method;
483-
}
484-
}
485-
486-
throw new Exception("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) does not exist!!?!?");
487-
}
488-
489-
// TODO: Use InvariantCulture where possible? Or is CurrentCulture fine because it's more flexible?
490-
private static MethodInfo? FindTryParseMethod(Type type)
491-
{
492-
static MethodInfo? Finder(Type type)
493-
{
494-
if (type.IsEnum)
495-
{
496-
return EnumTryParseMethod.MakeGenericMethod(type);
497-
}
498-
499-
var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
500-
501-
foreach (var method in staticMethods)
502-
{
503-
if (method.Name != "TryParse" || method.ReturnType != typeof(bool))
504-
{
505-
continue;
506-
}
507-
508-
var tryParseParameters = method.GetParameters();
509-
510-
if (tryParseParameters.Length == 2 &&
511-
tryParseParameters[0].ParameterType == typeof(string) &&
512-
tryParseParameters[1].IsOut &&
513-
tryParseParameters[1].ParameterType == type.MakeByRefType())
514-
{
515-
return method;
516-
}
517-
}
518-
519-
return null;
520-
}
521-
522-
return TryParseMethodCache.GetOrAdd(type, Finder);
523-
}
524-
525-
private static bool HasTryParseMethod(ParameterInfo parameter)
526-
{
527-
var nonNullableParameterType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
528-
return FindTryParseMethod(nonNullableParameterType) is not null;
529-
}
530-
531460
private static Expression GetValueFromProperty(Expression sourceExpression, string key)
532461
{
533462
var itemProperty = sourceExpression.Type.GetProperty("Item");
@@ -559,7 +488,7 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
559488
var isNotNullable = underlyingNullableType is null;
560489

561490
var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType;
562-
var tryParseMethod = FindTryParseMethod(nonNullableParameterType);
491+
var tryParseMethod = RequestDelegateFactoryUtilities.FindTryParseMethod(nonNullableParameterType);
563492

564493
if (tryParseMethod is null)
565494
{

src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingSource.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ public class BindingSource : IEquatable<BindingSource?>
7878
isGreedy: false,
7979
isFromRequest: true);
8080

81+
/// <summary>
82+
/// A <see cref="BindingSource"/> for the request url path or query string.
83+
/// </summary>
84+
public static readonly BindingSource PathOrQuery = new BindingSource(
85+
"PathOrQuery",
86+
"PathOrQuery",
87+
isGreedy: false,
88+
isFromRequest: true);
89+
8190
/// <summary>
8291
/// A <see cref="BindingSource"/> for request services.
8392
/// </summary>

src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ abstract Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata.TemplateHint.get ->
102102
static Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptorExtensions.GetProperty<T>(this Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor! actionDescriptor) -> T?
103103
static Microsoft.AspNetCore.Mvc.Formatters.InputFormatterResult.Success(object? model) -> Microsoft.AspNetCore.Mvc.Formatters.InputFormatterResult!
104104
static Microsoft.AspNetCore.Mvc.Formatters.InputFormatterResult.SuccessAsync(object? model) -> System.Threading.Tasks.Task<Microsoft.AspNetCore.Mvc.Formatters.InputFormatterResult!>!
105+
static readonly Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.PathOrQuery -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource!
105106
virtual Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext.Result.get -> Microsoft.AspNetCore.Mvc.IActionResult?
106107
virtual Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext.ActionArguments.get -> System.Collections.Generic.IDictionary<string!, object?>!
107108
virtual Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata.BoundConstructorInvoker.get -> System.Func<object?[]!, object!>?

src/Mvc/Mvc.ApiExplorer/src/EndpointMethodInfoApiDescriptionProvider.cs

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Linq;
56
using System.Reflection;
7+
using System.Threading;
68
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Metadata;
710
using Microsoft.AspNetCore.Mvc.Abstractions;
811
using Microsoft.AspNetCore.Mvc.ModelBinding;
912
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
@@ -78,6 +81,8 @@ private static ApiDescription CreateApiDescription(RoutePattern pattern, string
7881
responseType = awaitableInfo.ResultType;
7982
}
8083

84+
responseType = Nullable.GetUnderlyingType(responseType) ?? responseType;
85+
8186
if (CreateApiResponseType(responseType) is { } apiResponseType)
8287
{
8388
apiDescription.SupportedResponseTypes.Add(apiResponseType);
@@ -86,19 +91,62 @@ private static ApiDescription CreateApiDescription(RoutePattern pattern, string
8691
return apiDescription;
8792
}
8893

89-
private static ApiParameterDescription CreateApiParameterDescription(ParameterInfo parameterInfo)
94+
private static ApiParameterDescription CreateApiParameterDescription(ParameterInfo parameter)
9095
{
91-
var parameterType = parameterInfo.ParameterType;
96+
var parameterType = parameter.ParameterType;
97+
98+
var (source, name) = GetBindingSourceAndName(parameter);
9299

93100
return new ApiParameterDescription
94101
{
95-
Name = parameterInfo.Name ?? parameterType.Name,
102+
Name = name,
96103
ModelMetadata = new EndpointMethodInfoModelMetadata(ModelMetadataIdentity.ForType(parameterType)),
97-
Source = BindingSource.Path,
98-
DefaultValue = parameterInfo.DefaultValue,
104+
Source = source,
105+
DefaultValue = parameter.DefaultValue,
99106
};
100107
}
101108

109+
// TODO: Share more of this logic with RequestDelegateFactory.CreateArgument(...) using RequestDelegateFactoryUtilities
110+
// which is shared source.
111+
private static (BindingSource, string) GetBindingSourceAndName(ParameterInfo parameter)
112+
{
113+
var attributes = parameter.GetCustomAttributes();
114+
115+
if (attributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
116+
{
117+
return (BindingSource.Path, routeAttribute.Name ?? parameter.Name ?? string.Empty);
118+
}
119+
else if (attributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
120+
{
121+
return (BindingSource.Query, queryAttribute.Name ?? parameter.Name ?? string.Empty);
122+
}
123+
else if (attributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
124+
{
125+
return (BindingSource.Header, headerAttribute.Name ?? parameter.Name ?? string.Empty);
126+
}
127+
else if (parameter.CustomAttributes.Any(a => typeof(IFromBodyMetadata).IsAssignableFrom(a.AttributeType)))
128+
{
129+
return (BindingSource.Body, parameter.Name ?? string.Empty);
130+
}
131+
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) ||
132+
parameter.ParameterType == typeof(HttpContext) ||
133+
parameter.ParameterType == typeof(CancellationToken) ||
134+
parameter.ParameterType.IsInterface)
135+
{
136+
return (BindingSource.Body, parameter.Name ?? string.Empty);
137+
}
138+
else if (parameter.ParameterType == typeof(string) || RequestDelegateFactoryUtilities.HasTryParseMethod(parameter))
139+
{
140+
// TODO: Look at the pattern and infer whether it's really the path or query.
141+
// This cannot be done by RequestDelegateFactory currently because of the layering, but could be done here.
142+
return (BindingSource.PathOrQuery, parameter.Name ?? string.Empty);
143+
}
144+
else
145+
{
146+
return (BindingSource.Body, parameter.Name ?? string.Empty);
147+
}
148+
}
149+
102150
private static ApiResponseType? CreateApiResponseType(Type responseType)
103151
{
104152
if (typeof(IResult).IsAssignableFrom(responseType))
@@ -107,36 +155,35 @@ private static ApiParameterDescription CreateApiParameterDescription(ParameterIn
107155
// REVIEW: Is there any value in returning an ApiResponseType with StatusCode = 200 and that's it?
108156
return null;
109157
}
110-
111-
if (responseType == typeof(void))
158+
else if (responseType == typeof(void))
112159
{
113160
return new ApiResponseType
114161
{
115162
ModelMetadata = new EndpointMethodInfoModelMetadata(ModelMetadataIdentity.ForType(typeof(void))),
116163
StatusCode = 200,
117164
};
118165
}
119-
120-
if (responseType == typeof(string))
166+
else if (responseType == typeof(string))
121167
{
122168
// This uses HttpResponse.WriteAsync(string) method which doesn't set a content type. It could be anything,
123169
// but I think "text/plain" is a reasonable assumption.
124-
125170
return new ApiResponseType
126171
{
127172
ApiResponseFormats = { new ApiResponseFormat { MediaType = "text/plain" } },
128173
ModelMetadata = new EndpointMethodInfoModelMetadata(ModelMetadataIdentity.ForType(typeof(string))),
129174
StatusCode = 200,
130175
};
131176
}
132-
133-
// Everything else is written using HttpResponse.WriteAsJsonAsync<TValue>(T).
134-
return new ApiResponseType
177+
else
135178
{
136-
ApiResponseFormats = { new ApiResponseFormat { MediaType = "application/json" } },
137-
ModelMetadata = new EndpointMethodInfoModelMetadata(ModelMetadataIdentity.ForType(responseType)),
138-
StatusCode = 200,
139-
};
179+
// Everything else is written using HttpResponse.WriteAsJsonAsync<TValue>(T).
180+
return new ApiResponseType
181+
{
182+
ApiResponseFormats = { new ApiResponseFormat { MediaType = "application/json" } },
183+
ModelMetadata = new EndpointMethodInfoModelMetadata(ModelMetadataIdentity.ForType(responseType)),
184+
StatusCode = 200,
185+
};
186+
}
140187
}
141188
}
142189
}

src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
<IsPackable>false</IsPackable>
1010
</PropertyGroup>
1111

12+
<ItemGroup>
13+
<Compile Include="$(SharedSourceRoot)RequestDelegateFactoryUtilities.cs" />
14+
</ItemGroup>
15+
1216
<ItemGroup>
1317
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
1418
</ItemGroup>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Reflection;
7+
8+
namespace Microsoft.AspNetCore.Http
9+
{
10+
// REVIEW: Better name?
11+
internal static class RequestDelegateFactoryUtilities
12+
{
13+
private static readonly MethodInfo EnumTryParseMethod = GetEnumTryParseMethod();
14+
15+
// Since this is shared source the cache won't be shared between RequestDelegateFactory and the ApiDescriptionProvider sadly :(
16+
private static readonly ConcurrentDictionary<Type, MethodInfo?> TryParseMethodCache = new();
17+
18+
public static bool HasTryParseMethod(ParameterInfo parameter)
19+
{
20+
var nonNullableParameterType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
21+
return FindTryParseMethod(nonNullableParameterType) is not null;
22+
}
23+
24+
// TODO: Use InvariantCulture where possible? Or is CurrentCulture fine because it's more flexible?
25+
public static MethodInfo? FindTryParseMethod(Type type)
26+
{
27+
static MethodInfo? Finder(Type type)
28+
{
29+
if (type.IsEnum)
30+
{
31+
return EnumTryParseMethod.MakeGenericMethod(type);
32+
}
33+
34+
var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
35+
36+
foreach (var method in staticMethods)
37+
{
38+
if (method.Name != "TryParse" || method.ReturnType != typeof(bool))
39+
{
40+
continue;
41+
}
42+
43+
var tryParseParameters = method.GetParameters();
44+
45+
if (tryParseParameters.Length == 2 &&
46+
tryParseParameters[0].ParameterType == typeof(string) &&
47+
tryParseParameters[1].IsOut &&
48+
tryParseParameters[1].ParameterType == type.MakeByRefType())
49+
{
50+
return method;
51+
}
52+
}
53+
54+
return null;
55+
}
56+
57+
return TryParseMethodCache.GetOrAdd(type, Finder);
58+
}
59+
60+
private static MethodInfo GetEnumTryParseMethod()
61+
{
62+
var staticEnumMethods = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static);
63+
64+
foreach (var method in staticEnumMethods)
65+
{
66+
if (!method.IsGenericMethod || method.Name != "TryParse" || method.ReturnType != typeof(bool))
67+
{
68+
continue;
69+
}
70+
71+
var tryParseParameters = method.GetParameters();
72+
73+
if (tryParseParameters.Length == 2 &&
74+
tryParseParameters[0].ParameterType == typeof(string) &&
75+
tryParseParameters[1].IsOut)
76+
{
77+
return method;
78+
}
79+
}
80+
81+
throw new Exception("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) does not exist!!?!?");
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)