Skip to content

Commit d70439c

Browse files
authored
Infer that interface parameters are services (#31658)
1 parent e340205 commit d70439c

7 files changed

+95
-71
lines changed

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

+20-12
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Concurrent;
6-
using System.Diagnostics;
76
using System.IO;
87
using System.Linq;
98
using System.Linq.Expressions;
@@ -203,15 +202,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
203202
}
204203
else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
205204
{
206-
if (factoryContext.JsonRequestBodyType is not null)
207-
{
208-
throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
209-
}
210-
211-
factoryContext.JsonRequestBodyType = parameter.ParameterType;
212-
factoryContext.AllowEmptyRequestBody = bodyAttribute.AllowEmpty;
213-
214-
return Expression.Convert(BodyValueExpr, parameter.ParameterType);
205+
return BindParameterFromBody(parameter.ParameterType, bodyAttribute.AllowEmpty, factoryContext);
215206
}
216207
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
217208
{
@@ -229,10 +220,14 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
229220
{
230221
return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext);
231222
}
232-
else
223+
else if (parameter.ParameterType.IsInterface)
233224
{
234225
return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
235226
}
227+
else
228+
{
229+
return BindParameterFromBody(parameter.ParameterType, allowEmpty: false, factoryContext);
230+
}
236231
}
237232

238233
private static Expression CreateMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) =>
@@ -428,7 +423,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
428423
var invoker = Expression.Lambda<Func<object?, HttpContext, object?, Task>>(
429424
responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile();
430425

431-
var bodyType = factoryContext.JsonRequestBodyType!;
426+
var bodyType = factoryContext.JsonRequestBodyType;
432427
object? defaultBodyValue = null;
433428

434429
if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType)
@@ -627,6 +622,19 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
627622
return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext);
628623
}
629624

625+
private static Expression BindParameterFromBody(Type parameterType, bool allowEmpty, FactoryContext factoryContext)
626+
{
627+
if (factoryContext.JsonRequestBodyType is not null)
628+
{
629+
throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
630+
}
631+
632+
factoryContext.JsonRequestBodyType = parameterType;
633+
factoryContext.AllowEmptyRequestBody = allowEmpty;
634+
635+
return Expression.Convert(BodyValueExpr, parameterType);
636+
}
637+
630638
private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
631639
{
632640
var mc = (MethodCallExpression)expr.Body;

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

+51-35
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,8 @@ public static bool TryParse(string? value, out MyTryParsableRecord? result)
361361
[MemberData(nameof(TryParsableParameters))]
362362
public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue)
363363
{
364-
var invalidDataException = new InvalidDataException();
365-
var serviceCollection = new ServiceCollection();
366-
serviceCollection.AddSingleton(LoggerFactory);
367-
368364
var httpContext = new DefaultHttpContext();
369365
httpContext.Request.RouteValues["tryParsable"] = routeValue;
370-
httpContext.Features.Set<IHttpRequestLifetimeFeature>(new TestHttpRequestLifetimeFeature());
371-
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
372366

373367
var requestDelegate = RequestDelegateFactory.Create(action);
374368

@@ -416,7 +410,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR
416410
Assert.Equal(42, httpContext.Items["tryParsable"]);
417411
}
418412

419-
public static object[][] DelegatesWithInvalidAttributes
413+
public static object[][] DelegatesWithAttributesOnNotTryParsableParameters
420414
{
421415
get
422416
{
@@ -434,7 +428,7 @@ void InvalidFromHeader([FromHeader] object notTryParsable) { }
434428
}
435429

436430
[Theory]
437-
[MemberData(nameof(DelegatesWithInvalidAttributes))]
431+
[MemberData(nameof(DelegatesWithAttributesOnNotTryParsableParameters))]
438432
public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action)
439433
{
440434
var ex = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(action));
@@ -460,7 +454,6 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2)
460454
invoked = true;
461455
}
462456

463-
var invalidDataException = new InvalidDataException();
464457
var serviceCollection = new ServiceCollection();
465458
serviceCollection.AddSingleton(LoggerFactory);
466459

@@ -542,47 +535,61 @@ void TestAction([FromHeader(Name = customHeaderName)] int value)
542535
Assert.Equal(originalHeaderParam, deserializedRouteParam);
543536
}
544537

545-
[Fact]
546-
public async Task RequestDelegatePopulatesFromBodyParameter()
538+
public static object[][] FromBodyActions
547539
{
548-
Todo originalTodo = new()
540+
get
549541
{
550-
Name = "Write more tests!"
551-
};
542+
void TestExplicitFromBody(HttpContext httpContext, [FromBody] Todo todo)
543+
{
544+
httpContext.Items.Add("body", todo);
545+
}
552546

553-
Todo? deserializedRequestBody = null;
547+
void TestImpliedFromBody(HttpContext httpContext, Todo myService)
548+
{
549+
httpContext.Items.Add("body", myService);
550+
}
554551

555-
void TestAction([FromBody] Todo todo)
556-
{
557-
deserializedRequestBody = todo;
552+
return new[]
553+
{
554+
new[] { (Action<HttpContext, Todo>)TestExplicitFromBody },
555+
new[] { (Action<HttpContext, Todo>)TestImpliedFromBody },
556+
};
558557
}
558+
}
559+
560+
[Theory]
561+
[MemberData(nameof(FromBodyActions))]
562+
public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action)
563+
{
564+
Todo originalTodo = new()
565+
{
566+
Name = "Write more tests!"
567+
};
559568

560569
var httpContext = new DefaultHttpContext();
561570
httpContext.Request.Headers["Content-Type"] = "application/json";
562571

563572
var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo);
564573
httpContext.Request.Body = new MemoryStream(requestBodyBytes);
565574

566-
var requestDelegate = RequestDelegateFactory.Create((Action<Todo>)TestAction);
575+
var requestDelegate = RequestDelegateFactory.Create(action);
567576

568577
await requestDelegate(httpContext);
569578

579+
var deserializedRequestBody = httpContext.Items["body"];
570580
Assert.NotNull(deserializedRequestBody);
571-
Assert.Equal(originalTodo.Name, deserializedRequestBody!.Name);
581+
Assert.Equal(originalTodo.Name, ((Todo)deserializedRequestBody!).Name);
572582
}
573583

574-
[Fact]
575-
public async Task RequestDelegateRejectsEmptyBodyGivenDefaultFromBodyParameter()
584+
[Theory]
585+
[MemberData(nameof(FromBodyActions))]
586+
public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate action)
576587
{
577-
void TestAction([FromBody] Todo todo)
578-
{
579-
}
580-
581588
var httpContext = new DefaultHttpContext();
582589
httpContext.Request.Headers["Content-Type"] = "application/json";
583590
httpContext.Request.Headers["Content-Length"] = "0";
584591

585-
var requestDelegate = RequestDelegateFactory.Create((Action<Todo>)TestAction);
592+
var requestDelegate = RequestDelegateFactory.Create(action);
586593

587594
await Assert.ThrowsAsync<JsonException>(() => requestDelegate(httpContext));
588595
}
@@ -702,12 +709,16 @@ void TestAction([FromBody] Todo todo)
702709
[Fact]
703710
public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters()
704711
{
705-
void TestAction([FromBody] int value1, [FromBody] int value2) { }
712+
void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { }
713+
void TestInferredInvalidAction(Todo value1, Todo value2) { }
714+
void TestBothInvalidAction(Todo value1, [FromBody] int value2) { }
706715

707-
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int, int>)TestAction));
716+
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<int, int>)TestAttributedInvalidAction));
717+
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<Todo, Todo>)TestInferredInvalidAction));
718+
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create((Action<Todo, int>)TestBothInvalidAction));
708719
}
709720

710-
public static object[][] FromServiceParameter
721+
public static object[][] FromServiceActions
711722
{
712723
get
713724
{
@@ -716,7 +727,7 @@ void TestExplicitFromService(HttpContext httpContext, [FromService] MyService my
716727
httpContext.Items.Add("service", myService);
717728
}
718729

719-
void TestImpliedFromService(HttpContext httpContext, MyService myService)
730+
void TestImpliedFromService(HttpContext httpContext, IMyService myService)
720731
{
721732
httpContext.Items.Add("service", myService);
722733
}
@@ -730,13 +741,14 @@ void TestImpliedFromService(HttpContext httpContext, MyService myService)
730741
}
731742

732743
[Theory]
733-
[MemberData(nameof(FromServiceParameter))]
744+
[MemberData(nameof(FromServiceActions))]
734745
public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAttribute(Delegate action)
735746
{
736747
var myOriginalService = new MyService();
737748

738749
var serviceCollection = new ServiceCollection();
739750
serviceCollection.AddSingleton(myOriginalService);
751+
serviceCollection.AddSingleton<IMyService>(myOriginalService);
740752

741753
var httpContext = new DefaultHttpContext();
742754
httpContext.RequestServices = serviceCollection.BuildServiceProvider();
@@ -749,11 +761,11 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt
749761
}
750762

751763
[Theory]
752-
[MemberData(nameof(FromServiceParameter))]
764+
[MemberData(nameof(FromServiceActions))]
753765
public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Delegate action)
754766
{
755767
var httpContext = new DefaultHttpContext();
756-
httpContext.RequestServices = (new ServiceCollection()).BuildServiceProvider();
768+
httpContext.RequestServices = new ServiceCollection().BuildServiceProvider();
757769

758770
var requestDelegate = RequestDelegateFactory.Create((Action<HttpContext, MyService>)action);
759771

@@ -1058,7 +1070,11 @@ private class FromServiceAttribute : Attribute, IFromServiceMetadata
10581070
{
10591071
}
10601072

1061-
private class MyService
1073+
private interface IMyService
1074+
{
1075+
}
1076+
1077+
private class MyService : IMyService
10621078
{
10631079
}
10641080

src/Http/Routing/src/Builder/MapActionEndpointConventionBuilder.cs renamed to src/Http/Routing/src/Builder/MinimalActionEndpointConventionBuilder.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ namespace Microsoft.AspNetCore.Builder
99
/// <summary>
1010
/// Builds conventions that will be used for customization of MapAction <see cref="EndpointBuilder"/> instances.
1111
/// </summary>
12-
public sealed class MapActionEndpointConventionBuilder : IEndpointConventionBuilder
12+
public sealed class MinimalActionEndpointConventionBuilder : IEndpointConventionBuilder
1313
{
1414
private readonly List<IEndpointConventionBuilder> _endpointConventionBuilders;
1515

16-
internal MapActionEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder)
16+
internal MinimalActionEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder)
1717
{
1818
_endpointConventionBuilders = new List<IEndpointConventionBuilder>() { endpointConventionBuilder };
1919
}
2020

21-
internal MapActionEndpointConventionBuilder(List<IEndpointConventionBuilder> endpointConventionBuilders)
21+
internal MinimalActionEndpointConventionBuilder(List<IEndpointConventionBuilder> endpointConventionBuilders)
2222
{
2323
_endpointConventionBuilders = endpointConventionBuilders;
2424
}

src/Http/Routing/src/Builder/MapActionEndpointRouteBuilderExtensions.cs renamed to src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Builder
1414
/// <summary>
1515
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to define HTTP API endpoints.
1616
/// </summary>
17-
public static class MapActionEndpointRouteBuilderExtensions
17+
public static class MinmalActionEndpointRouteBuilderExtensions
1818
{
1919
// Avoid creating a new array every call
2020
private static readonly string[] GetVerb = new[] { "GET" };
@@ -30,7 +30,7 @@ public static class MapActionEndpointRouteBuilderExtensions
3030
/// <param name="pattern">The route pattern.</param>
3131
/// <param name="action">The delegate executed when the endpoint is matched.</param>
3232
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
33-
public static MapActionEndpointConventionBuilder MapGet(
33+
public static MinimalActionEndpointConventionBuilder MapGet(
3434
this IEndpointRouteBuilder endpoints,
3535
string pattern,
3636
Delegate action)
@@ -46,7 +46,7 @@ public static MapActionEndpointConventionBuilder MapGet(
4646
/// <param name="pattern">The route pattern.</param>
4747
/// <param name="action">The delegate executed when the endpoint is matched.</param>
4848
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
49-
public static MapActionEndpointConventionBuilder MapPost(
49+
public static MinimalActionEndpointConventionBuilder MapPost(
5050
this IEndpointRouteBuilder endpoints,
5151
string pattern,
5252
Delegate action)
@@ -62,7 +62,7 @@ public static MapActionEndpointConventionBuilder MapPost(
6262
/// <param name="pattern">The route pattern.</param>
6363
/// <param name="action">The delegate executed when the endpoint is matched.</param>
6464
/// <returns>A <see cref="IEndpointConventionBuilder"/> that canaction be used to further customize the endpoint.</returns>
65-
public static MapActionEndpointConventionBuilder MapPut(
65+
public static MinimalActionEndpointConventionBuilder MapPut(
6666
this IEndpointRouteBuilder endpoints,
6767
string pattern,
6868
Delegate action)
@@ -78,7 +78,7 @@ public static MapActionEndpointConventionBuilder MapPut(
7878
/// <param name="pattern">The route pattern.</param>
7979
/// <param name="action">The delegate executed when the endpoint is matched.</param>
8080
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
81-
public static MapActionEndpointConventionBuilder MapDelete(
81+
public static MinimalActionEndpointConventionBuilder MapDelete(
8282
this IEndpointRouteBuilder endpoints,
8383
string pattern,
8484
Delegate action)
@@ -95,7 +95,7 @@ public static MapActionEndpointConventionBuilder MapDelete(
9595
/// <param name="action">The delegate executed when the endpoint is matched.</param>
9696
/// <param name="httpMethods">HTTP methods that the endpoint will match.</param>
9797
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
98-
public static MapActionEndpointConventionBuilder MapMethods(
98+
public static MinimalActionEndpointConventionBuilder MapMethods(
9999
this IEndpointRouteBuilder endpoints,
100100
string pattern,
101101
IEnumerable<string> httpMethods,
@@ -120,7 +120,7 @@ public static MapActionEndpointConventionBuilder MapMethods(
120120
/// <param name="pattern">The route pattern.</param>
121121
/// <param name="action">The delegate executed when the endpoint is matched.</param>
122122
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
123-
public static MapActionEndpointConventionBuilder Map(
123+
public static MinimalActionEndpointConventionBuilder Map(
124124
this IEndpointRouteBuilder endpoints,
125125
string pattern,
126126
Delegate action)
@@ -136,7 +136,7 @@ public static MapActionEndpointConventionBuilder Map(
136136
/// <param name="pattern">The route pattern.</param>
137137
/// <param name="action">The delegate executed when the endpoint is matched.</param>
138138
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
139-
public static MapActionEndpointConventionBuilder Map(
139+
public static MinimalActionEndpointConventionBuilder Map(
140140
this IEndpointRouteBuilder endpoints,
141141
RoutePattern pattern,
142142
Delegate action)
@@ -185,7 +185,7 @@ public static MapActionEndpointConventionBuilder Map(
185185
endpoints.DataSources.Add(dataSource);
186186
}
187187

188-
return new MapActionEndpointConventionBuilder(dataSource.AddEndpointBuilder(builder));
188+
return new MinimalActionEndpointConventionBuilder(dataSource.AddEndpointBuilder(builder));
189189
}
190190
}
191191
}

0 commit comments

Comments
 (0)