Skip to content

Commit ff097c6

Browse files
authored
Support resolving keyed services from DI in RDF and RDG (#50095)
* Support resolving keyed services from DI in RDF and RDG * Address feedback from peer review * Support keyed services with different keys but same arg name
1 parent 35127bf commit ff097c6

File tree

51 files changed

+360
-81
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+360
-81
lines changed

src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,12 @@ internal static class DiagnosticDescriptors
106106
"Usage",
107107
DiagnosticSeverity.Warning,
108108
isEnabledByDefault: true);
109+
110+
public static DiagnosticDescriptor KeyedAndNotKeyedServiceAttributesNotSupported { get; } = new(
111+
"RDG013",
112+
new LocalizableResourceString(nameof(Resources.KeyedAndNotKeyedServiceAttributesNotSupported_Title), Resources.ResourceManager, typeof(Resources)),
113+
new LocalizableResourceString(nameof(Resources.KeyedAndNotKeyedServiceAttributesNotSupported_Message), Resources.ResourceManager, typeof(Resources)),
114+
"Usage",
115+
DiagnosticSeverity.Warning,
116+
isEnabledByDefault: true);
109117
}

src/Http/Http.Extensions/gen/Resources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,10 @@
189189
<data name="InaccessibleTypesNotSupported_Message" xml:space="preserve">
190190
<value>Encountered inaccessible type '{0}' while processing endpoint. Compile-time endpoint generation will skip this endpoint.</value>
191191
</data>
192+
<data name="KeyedAndNotKeyedServiceAttributesNotSupported_Title" xml:space="preserve">
193+
<value>Invalid source attributes</value>
194+
</data>
195+
<data name="KeyedAndNotKeyedServiceAttributesNotSupported_Message" xml:space="preserve">
196+
<value>The [FromKeyedServices] attribute is not supported on parameters that are also annotated with IFromServiceMetadata.</value>
197+
</data>
192198
</root>

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointEmitter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ internal static string EmitParameterPreparation(this IEnumerable<EndpointParamet
5454
case EndpointParameterSource.Service:
5555
parameter.EmitServiceParameterPreparation(parameterPreparationBuilder);
5656
break;
57+
case EndpointParameterSource.KeyedService:
58+
parameter.EmitKeyedServiceParameterPreparation(parameterPreparationBuilder);
59+
break;
5760
case EndpointParameterSource.AsParameters:
5861
parameter.EmitAsParametersParameterPreparation(parameterPreparationBuilder, emitterContext);
5962
break;

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ internal static void EmitServiceParameterPreparation(this EndpointParameter endp
324324
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {assigningCode};");
325325
}
326326

327+
internal static void EmitKeyedServiceParameterPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter)
328+
{
329+
codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());
330+
331+
var assigningCode = endpointParameter.IsOptional ?
332+
$"httpContext.RequestServices.GetKeyedService<{endpointParameter.Type}>({endpointParameter.KeyedServiceKey});" :
333+
$"httpContext.RequestServices.GetRequiredKeyedService<{endpointParameter.Type}>({endpointParameter.KeyedServiceKey})";
334+
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {assigningCode};");
335+
}
336+
327337
internal static void EmitAsParametersParameterPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter, EmitterContext emitterContext)
328338
{
329339
codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I
138138
else if (attributes.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata)))
139139
{
140140
Source = EndpointParameterSource.Service;
141+
if (attributes.TryGetAttribute(wellKnownTypes.Get(WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute), out var keyedServicesAttribute))
142+
{
143+
var location = endpoint.Operation.Syntax.GetLocation();
144+
endpoint.Diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.KeyedAndNotKeyedServiceAttributesNotSupported, location));
145+
}
146+
}
147+
else if (attributes.TryGetAttribute(wellKnownTypes.Get(WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute), out var keyedServicesAttribute))
148+
{
149+
Source = EndpointParameterSource.KeyedService;
150+
var constructorArgument = keyedServicesAttribute.ConstructorArguments.FirstOrDefault();
151+
KeyedServiceKey = SymbolDisplay.FormatPrimitive(constructorArgument.Value!, true, true);
141152
}
142153
else if (attributes.HasAttribute(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_AsParametersAttribute)))
143154
{
@@ -260,6 +271,7 @@ private static bool ImplementsIEndpointParameterMetadataProvider(ITypeSymbol typ
260271
public string? PropertyAsParameterInfoConstruction { get; set; }
261272
public IEnumerable<EndpointParameter>? EndpointParameters { get; set; }
262273
public bool IsFormFile { get; set; }
274+
public string? KeyedServiceKey { get; set; }
263275

264276
// Only used for SpecialType parameters that need
265277
// to be resolved by a specific WellKnownType
@@ -613,15 +625,17 @@ obj is EndpointParameter other &&
613625
other.SymbolName == SymbolName &&
614626
other.Ordinal == Ordinal &&
615627
other.IsOptional == IsOptional &&
616-
SymbolEqualityComparer.IncludeNullability.Equals(other.Type, Type);
628+
SymbolEqualityComparer.IncludeNullability.Equals(other.Type, Type) &&
629+
other.KeyedServiceKey == KeyedServiceKey;
617630

618631
public bool SignatureEquals(object obj) =>
619632
obj is EndpointParameter other &&
620633
SymbolEqualityComparer.IncludeNullability.Equals(other.Type, Type) &&
621634
// The name of the parameter matters when we are querying for a specific parameter using
622635
// an indexer, like `context.Request.RouteValues["id"]` or `context.Request.Query["id"]`
623636
// and when generating log messages for required bodies or services.
624-
other.SymbolName == SymbolName;
637+
other.SymbolName == SymbolName &&
638+
other.KeyedServiceKey == KeyedServiceKey;
625639

626640
public override int GetHashCode()
627641
{

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameterSource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal enum EndpointParameterSource
1414
JsonBodyOrService,
1515
FormBody,
1616
Service,
17+
KeyedService,
1718
// SpecialType refers to HttpContext, HttpRequest, CancellationToken, Stream, etc...
1819
// that are specially checked for in RequestDelegateFactory.CreateArgument()
1920
SpecialType,

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public static partial class RequestDelegateFactory
5656
private static readonly MethodInfo ExecuteAwaitedReturnMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteAwaitedReturn), BindingFlags.NonPublic | BindingFlags.Static)!;
5757
private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
5858
private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
59+
private static readonly MethodInfo GetRequiredKeyedServiceMethod = typeof(ServiceProviderKeyedServiceExtensions).GetMethod(nameof(ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider), typeof(object) })!;
60+
private static readonly MethodInfo GetKeyedServiceMethod = typeof(ServiceProviderKeyedServiceExtensions).GetMethod(nameof(ServiceProviderKeyedServiceExtensions.GetKeyedService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider), typeof(object) })!;
5961
private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
6062
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
6163
private static readonly MethodInfo StringIsNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), BindingFlags.Static | BindingFlags.Public)!;
@@ -761,9 +763,19 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
761763
}
762764
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
763765
{
766+
if (parameterCustomAttributes.OfType<FromKeyedServicesAttribute>().FirstOrDefault() is not null)
767+
{
768+
throw new NotSupportedException(
769+
$"The {nameof(FromKeyedServicesAttribute)} is not supported on parameters that are also annotated with {nameof(IFromServiceMetadata)}.");
770+
}
764771
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceAttribute);
765772
return BindParameterFromService(parameter, factoryContext);
766773
}
774+
else if (parameterCustomAttributes.OfType<FromKeyedServicesAttribute>().FirstOrDefault() is { } keyedServicesAttribute)
775+
{
776+
var key = keyedServicesAttribute.Key;
777+
return BindParameterFromKeyedService(parameter, key, factoryContext);
778+
}
767779
else if (parameterCustomAttributes.OfType<AsParametersAttribute>().Any())
768780
{
769781
if (parameter is PropertyAsParameterInfo)
@@ -1563,6 +1575,21 @@ private static Expression BindParameterFromService(ParameterInfo parameter, Requ
15631575
return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
15641576
}
15651577

1578+
private static Expression BindParameterFromKeyedService(ParameterInfo parameter, object key, RequestDelegateFactoryContext factoryContext)
1579+
{
1580+
var isOptional = IsOptionalParameter(parameter, factoryContext);
1581+
1582+
if (isOptional)
1583+
{
1584+
return Expression.Call(GetKeyedServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr, Expression.Convert(
1585+
Expression.Constant(key),
1586+
typeof(object)));
1587+
}
1588+
return Expression.Call(GetRequiredKeyedServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr, Expression.Convert(
1589+
Expression.Constant(key),
1590+
typeof(object)));
1591+
}
1592+
15661593
private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, RequestDelegateFactoryContext factoryContext, string source)
15671594
{
15681595
if (parameter.ParameterType == typeof(string) || parameter.ParameterType == typeof(string[])

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_NullableReturn.generated.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ namespace Microsoft.AspNetCore.Http.Generated
5858
private static readonly JsonOptions FallbackJsonOptions = new();
5959
private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get };
6060

61-
[InterceptsLocation(@"TestMapActions.cs", 25, 13)]
62-
[InterceptsLocation(@"TestMapActions.cs", 26, 5)]
61+
[InterceptsLocation(@"TestMapActions.cs", 26, 13)]
62+
[InterceptsLocation(@"TestMapActions.cs", 27, 5)]
6363
internal static RouteHandlerBuilder MapGet0(
6464
this IEndpointRouteBuilder endpoints,
6565
[StringSyntax("Route")] string pattern,
@@ -166,8 +166,8 @@ namespace Microsoft.AspNetCore.Http.Generated
166166
createRequestDelegate);
167167
}
168168

169-
[InterceptsLocation(@"TestMapActions.cs", 28, 5)]
170169
[InterceptsLocation(@"TestMapActions.cs", 29, 5)]
170+
[InterceptsLocation(@"TestMapActions.cs", 30, 5)]
171171
internal static RouteHandlerBuilder MapGet1(
172172
this IEndpointRouteBuilder endpoints,
173173
[StringSyntax("Route")] string pattern,

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Http.Generated
5858
private static readonly JsonOptions FallbackJsonOptions = new();
5959
private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get };
6060

61-
[InterceptsLocation(@"TestMapActions.cs", 25, 13)]
61+
[InterceptsLocation(@"TestMapActions.cs", 26, 13)]
6262
internal static RouteHandlerBuilder MapGet0(
6363
this IEndpointRouteBuilder endpoints,
6464
[StringSyntax("Route")] string pattern,
@@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Http.Generated
167167
createRequestDelegate);
168168
}
169169

170-
[InterceptsLocation(@"TestMapActions.cs", 26, 5)]
170+
[InterceptsLocation(@"TestMapActions.cs", 27, 5)]
171171
internal static RouteHandlerBuilder MapGet1(
172172
this IEndpointRouteBuilder endpoints,
173173
[StringSyntax("Route")] string pattern,
@@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Http.Generated
262262
createRequestDelegate);
263263
}
264264

265-
[InterceptsLocation(@"TestMapActions.cs", 27, 5)]
265+
[InterceptsLocation(@"TestMapActions.cs", 28, 5)]
266266
internal static RouteHandlerBuilder MapGet2(
267267
this IEndpointRouteBuilder endpoints,
268268
[StringSyntax("Route")] string pattern,
@@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.Http.Generated
359359
createRequestDelegate);
360360
}
361361

362-
[InterceptsLocation(@"TestMapActions.cs", 28, 5)]
362+
[InterceptsLocation(@"TestMapActions.cs", 29, 5)]
363363
internal static RouteHandlerBuilder MapGet3(
364364
this IEndpointRouteBuilder endpoints,
365365
[StringSyntax("Route")] string pattern,
@@ -454,7 +454,7 @@ namespace Microsoft.AspNetCore.Http.Generated
454454
createRequestDelegate);
455455
}
456456

457-
[InterceptsLocation(@"TestMapActions.cs", 29, 5)]
457+
[InterceptsLocation(@"TestMapActions.cs", 30, 5)]
458458
internal static RouteHandlerBuilder MapGet4(
459459
this IEndpointRouteBuilder endpoints,
460460
[StringSyntax("Route")] string pattern,
@@ -563,7 +563,7 @@ namespace Microsoft.AspNetCore.Http.Generated
563563
createRequestDelegate);
564564
}
565565

566-
[InterceptsLocation(@"TestMapActions.cs", 30, 5)]
566+
[InterceptsLocation(@"TestMapActions.cs", 31, 5)]
567567
internal static RouteHandlerBuilder MapGet5(
568568
this IEndpointRouteBuilder endpoints,
569569
[StringSyntax("Route")] string pattern,
@@ -658,7 +658,7 @@ namespace Microsoft.AspNetCore.Http.Generated
658658
createRequestDelegate);
659659
}
660660

661-
[InterceptsLocation(@"TestMapActions.cs", 31, 5)]
661+
[InterceptsLocation(@"TestMapActions.cs", 32, 5)]
662662
internal static RouteHandlerBuilder MapGet6(
663663
this IEndpointRouteBuilder endpoints,
664664
[StringSyntax("Route")] string pattern,
@@ -755,7 +755,7 @@ namespace Microsoft.AspNetCore.Http.Generated
755755
createRequestDelegate);
756756
}
757757

758-
[InterceptsLocation(@"TestMapActions.cs", 32, 5)]
758+
[InterceptsLocation(@"TestMapActions.cs", 33, 5)]
759759
internal static RouteHandlerBuilder MapGet7(
760760
this IEndpointRouteBuilder endpoints,
761761
[StringSyntax("Route")] string pattern,
@@ -850,7 +850,7 @@ namespace Microsoft.AspNetCore.Http.Generated
850850
createRequestDelegate);
851851
}
852852

853-
[InterceptsLocation(@"TestMapActions.cs", 33, 5)]
853+
[InterceptsLocation(@"TestMapActions.cs", 34, 5)]
854854
internal static RouteHandlerBuilder MapGet8(
855855
this IEndpointRouteBuilder endpoints,
856856
[StringSyntax("Route")] string pattern,
@@ -958,7 +958,7 @@ namespace Microsoft.AspNetCore.Http.Generated
958958
createRequestDelegate);
959959
}
960960

961-
[InterceptsLocation(@"TestMapActions.cs", 34, 5)]
961+
[InterceptsLocation(@"TestMapActions.cs", 35, 5)]
962962
internal static RouteHandlerBuilder MapGet9(
963963
this IEndpointRouteBuilder endpoints,
964964
[StringSyntax("Route")] string pattern,
@@ -1052,7 +1052,7 @@ namespace Microsoft.AspNetCore.Http.Generated
10521052
createRequestDelegate);
10531053
}
10541054

1055-
[InterceptsLocation(@"TestMapActions.cs", 35, 5)]
1055+
[InterceptsLocation(@"TestMapActions.cs", 36, 5)]
10561056
internal static RouteHandlerBuilder MapGet10(
10571057
this IEndpointRouteBuilder endpoints,
10581058
[StringSyntax("Route")] string pattern,
@@ -1148,7 +1148,7 @@ namespace Microsoft.AspNetCore.Http.Generated
11481148
createRequestDelegate);
11491149
}
11501150

1151-
[InterceptsLocation(@"TestMapActions.cs", 36, 5)]
1151+
[InterceptsLocation(@"TestMapActions.cs", 37, 5)]
11521152
internal static RouteHandlerBuilder MapGet11(
11531153
this IEndpointRouteBuilder endpoints,
11541154
[StringSyntax("Route")] string pattern,
@@ -1242,7 +1242,7 @@ namespace Microsoft.AspNetCore.Http.Generated
12421242
createRequestDelegate);
12431243
}
12441244

1245-
[InterceptsLocation(@"TestMapActions.cs", 37, 5)]
1245+
[InterceptsLocation(@"TestMapActions.cs", 38, 5)]
12461246
internal static RouteHandlerBuilder MapGet12(
12471247
this IEndpointRouteBuilder endpoints,
12481248
[StringSyntax("Route")] string pattern,
@@ -1351,7 +1351,7 @@ namespace Microsoft.AspNetCore.Http.Generated
13511351
createRequestDelegate);
13521352
}
13531353

1354-
[InterceptsLocation(@"TestMapActions.cs", 38, 5)]
1354+
[InterceptsLocation(@"TestMapActions.cs", 39, 5)]
13551355
internal static RouteHandlerBuilder MapGet13(
13561356
this IEndpointRouteBuilder endpoints,
13571357
[StringSyntax("Route")] string pattern,
@@ -1446,7 +1446,7 @@ namespace Microsoft.AspNetCore.Http.Generated
14461446
createRequestDelegate);
14471447
}
14481448

1449-
[InterceptsLocation(@"TestMapActions.cs", 39, 5)]
1449+
[InterceptsLocation(@"TestMapActions.cs", 40, 5)]
14501450
internal static RouteHandlerBuilder MapGet14(
14511451
this IEndpointRouteBuilder endpoints,
14521452
[StringSyntax("Route")] string pattern,
@@ -1554,7 +1554,7 @@ namespace Microsoft.AspNetCore.Http.Generated
15541554
createRequestDelegate);
15551555
}
15561556

1557-
[InterceptsLocation(@"TestMapActions.cs", 40, 5)]
1557+
[InterceptsLocation(@"TestMapActions.cs", 41, 5)]
15581558
internal static RouteHandlerBuilder MapGet15(
15591559
this IEndpointRouteBuilder endpoints,
15601560
[StringSyntax("Route")] string pattern,
@@ -1648,7 +1648,7 @@ namespace Microsoft.AspNetCore.Http.Generated
16481648
createRequestDelegate);
16491649
}
16501650

1651-
[InterceptsLocation(@"TestMapActions.cs", 41, 5)]
1651+
[InterceptsLocation(@"TestMapActions.cs", 42, 5)]
16521652
internal static RouteHandlerBuilder MapGet16(
16531653
this IEndpointRouteBuilder endpoints,
16541654
[StringSyntax("Route")] string pattern,
@@ -1757,7 +1757,7 @@ namespace Microsoft.AspNetCore.Http.Generated
17571757
createRequestDelegate);
17581758
}
17591759

1760-
[InterceptsLocation(@"TestMapActions.cs", 42, 5)]
1760+
[InterceptsLocation(@"TestMapActions.cs", 43, 5)]
17611761
internal static RouteHandlerBuilder MapGet17(
17621762
this IEndpointRouteBuilder endpoints,
17631763
[StringSyntax("Route")] string pattern,
@@ -1852,7 +1852,7 @@ namespace Microsoft.AspNetCore.Http.Generated
18521852
createRequestDelegate);
18531853
}
18541854

1855-
[InterceptsLocation(@"TestMapActions.cs", 43, 5)]
1855+
[InterceptsLocation(@"TestMapActions.cs", 44, 5)]
18561856
internal static RouteHandlerBuilder MapGet18(
18571857
this IEndpointRouteBuilder endpoints,
18581858
[StringSyntax("Route")] string pattern,
@@ -1960,7 +1960,7 @@ namespace Microsoft.AspNetCore.Http.Generated
19601960
createRequestDelegate);
19611961
}
19621962

1963-
[InterceptsLocation(@"TestMapActions.cs", 44, 5)]
1963+
[InterceptsLocation(@"TestMapActions.cs", 45, 5)]
19641964
internal static RouteHandlerBuilder MapGet19(
19651965
this IEndpointRouteBuilder endpoints,
19661966
[StringSyntax("Route")] string pattern,

0 commit comments

Comments
 (0)