Skip to content

Commit 6a09fd8

Browse files
authored
Added new overloads to RequestDelegateFactory (#34375)
- Throw exceptions if route parameters are specified via attributes but not declared in the list of route parameters. - Don't fall back to the query string if the the route parameter is specified. - Added tests
1 parent 19f5f1c commit 6a09fd8

File tree

6 files changed

+251
-90
lines changed

6 files changed

+251
-90
lines changed

src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ Microsoft.AspNetCore.Http.Headers.ResponseHeaders.SetCookie.get -> System.Collec
157157
Microsoft.AspNetCore.Http.Headers.ResponseHeaders.SetCookie.set -> void
158158
Microsoft.AspNetCore.Http.Headers.ResponseHeaders.SetList<T>(string! name, System.Collections.Generic.IList<T>? values) -> void
159159
Microsoft.AspNetCore.Http.RequestDelegateFactory
160+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions
161+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RequestDelegateFactoryOptions() -> void
162+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteParameterNames.get -> System.Collections.Generic.IEnumerable<string!>?
163+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteParameterNames.init -> void
164+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ServiceProvider.get -> System.IServiceProvider?
165+
Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ServiceProvider.init -> void
160166
Microsoft.AspNetCore.Mvc.ProblemDetails
161167
Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string?
162168
Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void
@@ -186,9 +192,8 @@ static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.AppendList<T>(th
186192
static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpRequest! request) -> Microsoft.AspNetCore.Http.Headers.RequestHeaders!
187193
static Microsoft.AspNetCore.Http.HeaderDictionaryTypeExtensions.GetTypedHeaders(this Microsoft.AspNetCore.Http.HttpResponse! response) -> Microsoft.AspNetCore.Http.Headers.ResponseHeaders!
188194
static Microsoft.AspNetCore.Http.HttpContextServerVariableExtensions.GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext! context, string! variableName) -> string?
189-
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, System.IServiceProvider? serviceProvider) -> Microsoft.AspNetCore.Http.RequestDelegate!
190-
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.IServiceProvider? serviceProvider) -> Microsoft.AspNetCore.Http.RequestDelegate!
191-
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.IServiceProvider? serviceProvider, System.Func<Microsoft.AspNetCore.Http.HttpContext!, object!>! targetFactory) -> Microsoft.AspNetCore.Http.RequestDelegate!
195+
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Delegate! action, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate!
196+
static Microsoft.AspNetCore.Http.RequestDelegateFactory.Create(System.Reflection.MethodInfo! methodInfo, System.Func<Microsoft.AspNetCore.Http.HttpContext!, object!>? targetFactory = null, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions? options = null) -> Microsoft.AspNetCore.Http.RequestDelegate!
192197
static Microsoft.AspNetCore.Http.ResponseExtensions.Clear(this Microsoft.AspNetCore.Http.HttpResponse! response) -> void
193198
static Microsoft.AspNetCore.Http.ResponseExtensions.Redirect(this Microsoft.AspNetCore.Http.HttpResponse! response, string! location, bool permanent, bool preserveMethod) -> void
194199
static Microsoft.AspNetCore.Http.SendFileResponseExtensions.SendFileAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, Microsoft.Extensions.FileProviders.IFileInfo! file, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!

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

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ public static partial class RequestDelegateFactory
6060
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
6161
/// </summary>
6262
/// <param name="action">A request handler with any number of custom parameters that often produces a response with its return value.</param>
63-
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> instance used to detect which parameters are services.</param>
63+
/// <param name="options">The <see cref="RequestDelegateFactoryOptions"/> used to configure the behavior of the handler.</param>
6464
/// <returns>The <see cref="RequestDelegate"/>.</returns>
65-
public static RequestDelegate Create(Delegate action, IServiceProvider? serviceProvider)
65+
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
66+
public static RequestDelegate Create(Delegate action, RequestDelegateFactoryOptions? options = null)
67+
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
6668
{
6769
if (action is null)
6870
{
@@ -75,69 +77,60 @@ public static RequestDelegate Create(Delegate action, IServiceProvider? serviceP
7577
null => null,
7678
};
7779

78-
var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, serviceProvider, targetExpression);
80+
var targetableRequestDelegate = CreateTargetableRequestDelegate(action.Method, options, targetExpression);
7981

8082
return httpContext =>
8183
{
8284
return targetableRequestDelegate(action.Target, httpContext);
8385
};
8486
}
8587

86-
/// <summary>
87-
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="methodInfo"/>.
88-
/// </summary>
89-
/// <param name="methodInfo">A static request handler with any number of custom parameters that often produces a response with its return value.</param>
90-
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> instance used to detect which parameters are services.</param>
91-
/// <returns>The <see cref="RequestDelegate"/>.</returns>
92-
public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? serviceProvider)
93-
{
94-
if (methodInfo is null)
95-
{
96-
throw new ArgumentNullException(nameof(methodInfo));
97-
}
98-
99-
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, serviceProvider, targetExpression: null);
100-
101-
return httpContext =>
102-
{
103-
return targetableRequestDelegate(null, httpContext);
104-
};
105-
}
106-
10788
/// <summary>
10889
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="methodInfo"/>.
10990
/// </summary>
11091
/// <param name="methodInfo">A request handler with any number of custom parameters that often produces a response with its return value.</param>
111-
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> instance used to detect which parameters are services.</param>
11292
/// <param name="targetFactory">Creates the <see langword="this"/> for the non-static method.</param>
93+
/// <param name="options">The <see cref="RequestDelegateFactoryOptions"/> used to configure the behavior of the handler.</param>
11394
/// <returns>The <see cref="RequestDelegate"/>.</returns>
114-
public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? serviceProvider, Func<HttpContext, object> targetFactory)
95+
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
96+
public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, object>? targetFactory = null, RequestDelegateFactoryOptions? options = null)
97+
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
11598
{
11699
if (methodInfo is null)
117100
{
118101
throw new ArgumentNullException(nameof(methodInfo));
119102
}
120103

121-
if (targetFactory is null)
104+
if (methodInfo.DeclaringType is null)
122105
{
123-
throw new ArgumentNullException(nameof(targetFactory));
106+
throw new ArgumentException($"{nameof(methodInfo)} does not have a declaring type.");
124107
}
125108

126-
if (methodInfo.DeclaringType is null)
109+
if (targetFactory is null)
127110
{
128-
throw new ArgumentException($"A {nameof(targetFactory)} was provided, but {nameof(methodInfo)} does not have a Declaring type.");
111+
if (methodInfo.IsStatic)
112+
{
113+
var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression: null);
114+
115+
return httpContext =>
116+
{
117+
return untargetableRequestDelegate(null, httpContext);
118+
};
119+
}
120+
121+
targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!;
129122
}
130123

131124
var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType);
132-
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, serviceProvider, targetExpression);
125+
var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, targetExpression);
133126

134127
return httpContext =>
135128
{
136129
return targetableRequestDelegate(targetFactory(httpContext), httpContext);
137130
};
138131
}
139132

140-
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, IServiceProvider? serviceProvider, Expression? targetExpression)
133+
private static Func<object?, HttpContext, Task> CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, Expression? targetExpression)
141134
{
142135
// Non void return type
143136

@@ -157,9 +150,14 @@ public static RequestDelegate Create(MethodInfo methodInfo, IServiceProvider? se
157150

158151
var factoryContext = new FactoryContext()
159152
{
160-
ServiceProviderIsService = serviceProvider?.GetService<IServiceProviderIsService>()
153+
ServiceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>()
161154
};
162155

156+
if (options?.RouteParameterNames is { } routeParameterNames)
157+
{
158+
factoryContext.RouteParameters = new(routeParameterNames);
159+
}
160+
163161
var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext);
164162

165163
var responseWritingMethodCall = factoryContext.TryParseParams.Count > 0 ?
@@ -202,6 +200,11 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
202200

203201
if (parameterCustomAttributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
204202
{
203+
if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(parameter.Name))
204+
{
205+
throw new InvalidOperationException($"{parameter.Name} is not a route paramter.");
206+
}
207+
205208
return BindParameterFromProperty(parameter, RouteValuesExpr, routeAttribute.Name ?? parameter.Name, factoryContext);
206209
}
207210
else if (parameterCustomAttributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
@@ -242,6 +245,13 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
242245
}
243246
else if (parameter.ParameterType == typeof(string) || TryParseMethodCache.HasTryParseMethod(parameter))
244247
{
248+
// We're in the fallback case and we have a parameter and route parameter match so don't fallback
249+
// to query string in this case
250+
if (factoryContext.RouteParameters is { } routeParams && routeParams.Contains(parameter.Name))
251+
{
252+
return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext);
253+
}
254+
245255
return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext);
246256
}
247257
else
@@ -814,6 +824,7 @@ private class FactoryContext
814824
public Type? JsonRequestBodyType { get; set; }
815825
public bool AllowEmptyRequestBody { get; set; }
816826
public IServiceProviderIsService? ServiceProviderIsService { get; init; }
827+
public List<string>? RouteParameters { get; set; }
817828

818829
public bool UsingTempSourceString { get; set; }
819830
public List<(ParameterExpression, Expression)> TryParseParams { get; } = new();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
namespace Microsoft.AspNetCore.Http
5+
{
6+
/// <summary>
7+
/// Options for controlling the behavior
8+
/// </summary>
9+
public class RequestDelegateFactoryOptions
10+
{
11+
/// <summary>
12+
/// The <see cref="IServiceProvider"/> instance used to detect if handler parameters are services.
13+
/// </summary>
14+
public IServiceProvider? ServiceProvider { get; init; }
15+
16+
/// <summary>
17+
/// The list of route parameter names that are specified for this handler.
18+
/// </summary>
19+
public IEnumerable<string>? RouteParameterNames { get; init; }
20+
}
21+
}

0 commit comments

Comments
 (0)