Skip to content

Commit 965a8df

Browse files
authored
Expand FromX support and add route vs. query param inference (#46715)
* Expand FromX support and add route vs. query param inference * Fix incrementality test * Move code emissions out of parsing logic * Address feedback from peer review - Add support for EmpyBodyBehavior check - Update source snapshots to implicit tests - Avoid codegenning `TryResolveRouteOrQuery` if not needed - Clean up test types * Remove string interpolation in source under test * Address review feedback - Guard parameter name from attribute against null values - Support FromBody with AllowEmpty named argument * Undo format change
1 parent d2bab51 commit 965a8df

23 files changed

+2507
-261
lines changed

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Linq;
56
using System.Text;
67

78
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
@@ -21,8 +22,14 @@ internal static string EmitParameterPreparation(this Endpoint endpoint)
2122
Source: EndpointParameterSource.SpecialType
2223
} => parameter.EmitSpecialParameterPreparation(),
2324
{
24-
Source: EndpointParameterSource.Query,
25-
} => parameter.EmitQueryParameterPreparation(),
25+
Source: EndpointParameterSource.Query or EndpointParameterSource.Header,
26+
} => parameter.EmitQueryOrHeaderParameterPreparation(),
27+
{
28+
Source: EndpointParameterSource.Route,
29+
} => parameter.EmitRouteParameterPreparation(),
30+
{
31+
Source: EndpointParameterSource.RouteOrQuery
32+
} => parameter.EmitRouteOrQueryParameterPreparation(),
2633
{
2734
Source: EndpointParameterSource.JsonBody
2835
} => parameter.EmitJsonBodyParameterPreparationString(),
@@ -45,4 +52,6 @@ internal static string EmitParameterPreparation(this Endpoint endpoint)
4552

4653
return parameterPreparationBuilder.ToString();
4754
}
55+
56+
public static string EmitArgumentList(this Endpoint endpoint) => string.Join(", ", endpoint.Parameters.Select(p => p.EmitArgument()));
4857
}

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

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Text;
56

67
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
@@ -9,22 +10,22 @@ internal static class EndpointParameterEmitter
910
internal static string EmitSpecialParameterPreparation(this EndpointParameter endpointParameter)
1011
{
1112
return $"""
12-
var {endpointParameter.Name}_local = {endpointParameter.AssigningCode};
13+
var {endpointParameter.EmitHandlerArgument()} = {endpointParameter.AssigningCode};
1314
""";
1415
}
1516

16-
internal static string EmitQueryParameterPreparation(this EndpointParameter endpointParameter)
17+
internal static string EmitQueryOrHeaderParameterPreparation(this EndpointParameter endpointParameter)
1718
{
1819
var builder = new StringBuilder();
19-
20-
// Preamble for diagnostics purposes.
2120
builder.AppendLine($"""
2221
{endpointParameter.EmitParameterDiagnosticComment()}
2322
""");
2423

25-
// Grab raw input from HttpContext.
24+
var assigningCode = endpointParameter.Source is EndpointParameterSource.Header
25+
? $"httpContext.Request.Headers[\"{endpointParameter.Name}\"]"
26+
: $"httpContext.Request.Query[\"{endpointParameter.Name}\"]";
2627
builder.AppendLine($$"""
27-
var {{endpointParameter.Name}}_raw = {{endpointParameter.AssigningCode}};
28+
var {{endpointParameter.EmitAssigningCodeResult()}} = {{assigningCode}};
2829
""");
2930

3031
// If we are not optional, then at this point we can just assign the string value to the handler argument,
@@ -34,35 +35,103 @@ internal static string EmitQueryParameterPreparation(this EndpointParameter endp
3435
if (endpointParameter.IsOptional)
3536
{
3637
builder.AppendLine($$"""
37-
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.Count > 0 ? {{endpointParameter.Name}}_raw.ToString() : null;
38+
var {{endpointParameter.EmitHandlerArgument()}} = {{endpointParameter.EmitAssigningCodeResult()}}.Count > 0 ? {{endpointParameter.EmitAssigningCodeResult()}}.ToString() : null;
3839
""");
3940
}
4041
else
4142
{
4243
builder.AppendLine($$"""
43-
if (StringValues.IsNullOrEmpty({{endpointParameter.Name}}_raw))
44+
if (StringValues.IsNullOrEmpty({{endpointParameter.EmitAssigningCodeResult()}}))
4445
{
4546
wasParamCheckFailure = true;
4647
}
47-
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.ToString();
48+
var {{endpointParameter.EmitHandlerArgument()}} = {{endpointParameter.EmitAssigningCodeResult()}}.ToString();
4849
""");
4950
}
5051

5152
return builder.ToString();
5253
}
5354

54-
internal static string EmitJsonBodyParameterPreparationString(this EndpointParameter endpointParameter)
55+
internal static string EmitRouteParameterPreparation(this EndpointParameter endpointParameter)
5556
{
5657
var builder = new StringBuilder();
58+
builder.AppendLine($"""
59+
{endpointParameter.EmitParameterDiagnosticComment()}
60+
""");
5761

58-
// Preamble for diagnostics purposes.
62+
// Throw an exception of if the route parameter name that was specific in the `FromRoute`
63+
// attribute or in the parameter name does not appear in the actual route.
64+
builder.AppendLine($$"""
65+
if (options?.RouteParameterNames?.Contains("{{endpointParameter.Name}}", StringComparer.OrdinalIgnoreCase) != true)
66+
{
67+
throw new InvalidOperationException($"'{{endpointParameter.Name}}' is not a route parameter.");
68+
}
69+
""");
70+
71+
var assigningCode = $"httpContext.Request.RouteValues[\"{endpointParameter.Name}\"]?.ToString()";
72+
builder.AppendLine($$"""
73+
var {{endpointParameter.EmitAssigningCodeResult()}} = {{assigningCode}};
74+
""");
75+
76+
if (!endpointParameter.IsOptional)
77+
{
78+
builder.AppendLine($$"""
79+
if ({{endpointParameter.EmitAssigningCodeResult()}} == null)
80+
{
81+
wasParamCheckFailure = true;
82+
}
83+
""");
84+
}
85+
builder.AppendLine($"""
86+
var {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitAssigningCodeResult()};
87+
""");
88+
89+
return builder.ToString();
90+
}
91+
92+
internal static string EmitRouteOrQueryParameterPreparation(this EndpointParameter endpointParameter)
93+
{
94+
var builder = new StringBuilder();
5995
builder.AppendLine($"""
6096
{endpointParameter.EmitParameterDiagnosticComment()}
6197
""");
6298

63-
// Grab raw input from HttpContext.
99+
var parameterName = endpointParameter.Name;
100+
var assigningCode = $@"options?.RouteParameterNames?.Contains(""{parameterName}"", StringComparer.OrdinalIgnoreCase) == true";
101+
assigningCode += $@"? new StringValues(httpContext.Request.RouteValues[$""{parameterName}""]?.ToString())";
102+
assigningCode += $@": httpContext.Request.Query[$""{parameterName}""];";
103+
64104
builder.AppendLine($$"""
65-
var (isSuccessful, {{endpointParameter.Name}}_local) = {{endpointParameter.AssigningCode}};
105+
var {{endpointParameter.EmitAssigningCodeResult()}} = {{assigningCode}};
106+
""");
107+
108+
if (!endpointParameter.IsOptional)
109+
{
110+
builder.AppendLine($$"""
111+
if ({{endpointParameter.EmitAssigningCodeResult()}} is StringValues { Count: 0 })
112+
{
113+
wasParamCheckFailure = true;
114+
}
115+
""");
116+
}
117+
118+
builder.AppendLine($"""
119+
var {endpointParameter.EmitHandlerArgument()} = {endpointParameter.EmitAssigningCodeResult()};
120+
""");
121+
122+
return builder.ToString();
123+
}
124+
125+
internal static string EmitJsonBodyParameterPreparationString(this EndpointParameter endpointParameter)
126+
{
127+
var builder = new StringBuilder();
128+
builder.AppendLine($"""
129+
{endpointParameter.EmitParameterDiagnosticComment()}
130+
""");
131+
132+
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBody<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")})";
133+
builder.AppendLine($$"""
134+
var (isSuccessful, {{endpointParameter.EmitHandlerArgument()}}) = {{assigningCode}};
66135
""");
67136

68137
// If binding from the JSON body fails, we exit early. Don't
@@ -88,14 +157,32 @@ internal static string EmitServiceParameterPreparation(this EndpointParameter en
88157
""");
89158

90159
// Requiredness checks for services are handled by the distinction
91-
// between GetRequiredService and GetService in the AssigningCode.
160+
// between GetRequiredService and GetService in the assigningCode.
161+
// Unlike other scenarios, this will result in an exception being thrown
162+
// at runtime.
163+
var assigningCode = endpointParameter.IsOptional ?
164+
$"httpContext.RequestServices.GetService<{endpointParameter.Type}>();" :
165+
$"httpContext.RequestServices.GetRequiredService<{endpointParameter.Type}>()";
166+
92167
builder.AppendLine($$"""
93-
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.AssigningCode}};
168+
var {{endpointParameter.EmitHandlerArgument()}} = {{assigningCode}};
94169
""");
95170

96171
return builder.ToString();
97172
}
98173

99174
private static string EmitParameterDiagnosticComment(this EndpointParameter endpointParameter) =>
100-
$"// Endpoint Parameter: {endpointParameter.Name} (Type = {endpointParameter.Type}, IsOptional = {endpointParameter.IsOptional}, Source = {endpointParameter.Source})";
175+
$"// Endpoint Parameter: {endpointParameter.Name} (Type = {endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}, IsOptional = {endpointParameter.IsOptional}, Source = {endpointParameter.Source})";
176+
177+
private static string EmitHandlerArgument(this EndpointParameter endpointParameter) => $"{endpointParameter.Name}_local";
178+
private static string EmitAssigningCodeResult(this EndpointParameter endpointParameter) => $"{endpointParameter.Name}_raw";
179+
180+
public static string EmitArgument(this EndpointParameter endpointParameter) => endpointParameter.Source switch
181+
{
182+
EndpointParameterSource.JsonBody or EndpointParameterSource.Route or EndpointParameterSource.RouteOrQuery => endpointParameter.IsOptional
183+
? endpointParameter.EmitHandlerArgument()
184+
: $"{endpointParameter.EmitHandlerArgument()}!",
185+
EndpointParameterSource.Unknown => throw new Exception("Unreachable!"),
186+
_ => endpointParameter.EmitHandlerArgument()
187+
};
101188
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
1313

1414
internal class Endpoint
1515
{
16-
private string? _argumentListCache;
17-
1816
public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
1917
{
2018
Operation = operation;
@@ -67,8 +65,6 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
6765
public string? RoutePattern { get; }
6866
public EndpointResponse? Response { get; }
6967
public EndpointParameter[] Parameters { get; } = Array.Empty<EndpointParameter>();
70-
public string EmitArgumentList() => _argumentListCache ??= string.Join(", ", Parameters.Select(p => p.EmitArgument()));
71-
7268
public List<DiagnosticDescriptor> Diagnostics { get; } = new List<DiagnosticDescriptor>();
7369

7470
public (string File, int LineNumber) Location { get; }
@@ -91,7 +87,7 @@ public static bool SignatureEquals(Endpoint a, Endpoint b)
9187

9288
for (var i = 0; i < a.Parameters.Length; i++)
9389
{
94-
if (!a.Parameters[i].Equals(b.Parameters[i]))
90+
if (!a.Parameters[i].SignatureEquals(b.Parameters[i]))
9591
{
9692
return false;
9793
}
@@ -108,7 +104,7 @@ public static int GetSignatureHashCode(Endpoint endpoint)
108104

109105
foreach (var parameter in endpoint.Parameters)
110106
{
111-
hashCode.Add(parameter);
107+
hashCode.Add(parameter.Type, SymbolEqualityComparer.Default);
112108
}
113109

114110
return hashCode.ToHashCode();

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

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,50 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
1717
Type = parameter.Type;
1818
Name = parameter.Name;
1919
Source = EndpointParameterSource.Unknown;
20-
HandlerArgument = $"{parameter.Name}_local";
2120

2221
var fromQueryMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata);
2322
var fromServiceMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
23+
var fromRouteMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata);
24+
var fromHeaderMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata);
2425

25-
if (parameter.HasAttributeImplementingInterface(fromQueryMetadataInterfaceType))
26+
if (parameter.HasAttributeImplementingInterface(fromRouteMetadataInterfaceType, out var fromRouteAttribute))
27+
{
28+
Source = EndpointParameterSource.Route;
29+
Name = GetParameterName(fromRouteAttribute, parameter.Name);
30+
IsOptional = parameter.IsOptional();
31+
}
32+
else if (parameter.HasAttributeImplementingInterface(fromQueryMetadataInterfaceType, out var fromQueryAttribute))
2633
{
2734
Source = EndpointParameterSource.Query;
28-
AssigningCode = $"httpContext.Request.Query[\"{parameter.Name}\"]";
29-
IsOptional = parameter.Type is INamedTypeSymbol
30-
{
31-
NullableAnnotation: NullableAnnotation.Annotated
32-
};
35+
Name = GetParameterName(fromQueryAttribute, parameter.Name);
36+
IsOptional = parameter.IsOptional();
37+
}
38+
else if (parameter.HasAttributeImplementingInterface(fromHeaderMetadataInterfaceType, out var fromHeaderAttribute))
39+
{
40+
Source = EndpointParameterSource.Header;
41+
Name = GetParameterName(fromHeaderAttribute, parameter.Name);
42+
IsOptional = parameter.IsOptional();
3343
}
34-
else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var jsonBodyAssigningCode, out var isOptional))
44+
else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var isOptional))
3545
{
3646
Source = EndpointParameterSource.JsonBody;
37-
AssigningCode = jsonBodyAssigningCode;
3847
IsOptional = isOptional;
3948
}
4049
else if (parameter.HasAttributeImplementingInterface(fromServiceMetadataInterfaceType))
4150
{
4251
Source = EndpointParameterSource.Service;
4352
IsOptional = parameter.Type is INamedTypeSymbol { NullableAnnotation: NullableAnnotation.Annotated } || parameter.HasExplicitDefaultValue;
44-
AssigningCode = IsOptional ?
45-
$"httpContext.RequestServices.GetService<{parameter.Type}>();" :
46-
$"httpContext.RequestServices.GetRequiredService<{parameter.Type}>()";
4753
}
4854
else if (TryGetSpecialTypeAssigningCode(Type, wellKnownTypes, out var specialTypeAssigningCode))
4955
{
5056
Source = EndpointParameterSource.SpecialType;
5157
AssigningCode = specialTypeAssigningCode;
5258
}
59+
else if (parameter.Type.SpecialType == SpecialType.System_String)
60+
{
61+
Source = EndpointParameterSource.RouteOrQuery;
62+
IsOptional = parameter.IsOptional();
63+
}
5364
else
5465
{
5566
// TODO: Inferencing rules go here - but for now:
@@ -60,19 +71,12 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
6071
public ITypeSymbol Type { get; }
6172
public EndpointParameterSource Source { get; }
6273

63-
// TODO: If the parameter has [FromRoute("AnotherName")] or similar, prefer that.
74+
// Only used for SpecialType parameters that need
75+
// to be resolved by a specific WellKnownType
76+
internal string? AssigningCode { get; set; }
6477
public string Name { get; }
65-
public string? AssigningCode { get; }
66-
public string HandlerArgument { get; }
6778
public bool IsOptional { get; }
6879

69-
public string EmitArgument() => Source switch
70-
{
71-
EndpointParameterSource.SpecialType or EndpointParameterSource.Query or EndpointParameterSource.Service => HandlerArgument,
72-
EndpointParameterSource.JsonBody => IsOptional ? HandlerArgument : $"{HandlerArgument}!",
73-
_ => throw new Exception("Unreachable!")
74-
};
75-
7680
// TODO: Handle special form types like IFormFileCollection that need special body-reading logic.
7781
private static bool TryGetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out string? callingCode)
7882
{
@@ -118,33 +122,34 @@ private static bool TryGetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTy
118122

119123
private static bool TryGetExplicitFromJsonBody(IParameterSymbol parameter,
120124
WellKnownTypes wellKnownTypes,
121-
[NotNullWhen(true)] out string? assigningCode,
122125
out bool isOptional)
123126
{
124-
assigningCode = null;
125127
isOptional = false;
126-
if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata), out var fromBodyAttribute))
127-
{
128-
foreach (var namedArgument in fromBodyAttribute.NamedArguments)
129-
{
130-
if (namedArgument.Key == "AllowEmpty")
131-
{
132-
isOptional |= namedArgument.Value.Value is true;
133-
}
134-
}
135-
isOptional |= (parameter.NullableAnnotation == NullableAnnotation.Annotated || parameter.HasExplicitDefaultValue);
136-
assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBody<{parameter.Type}>(httpContext, {(isOptional ? "true" : "false")})";
137-
return true;
128+
if (!parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata), out var fromBodyAttribute))
129+
{
130+
return false;
138131
}
139-
return false;
132+
isOptional |= fromBodyAttribute.TryGetNamedArgumentValue<int>("EmptyBodyBehavior", out var emptyBodyBehaviorValue) && emptyBodyBehaviorValue == 1;
133+
isOptional |= fromBodyAttribute.TryGetNamedArgumentValue<bool>("AllowEmpty", out var allowEmptyValue) && allowEmptyValue;
134+
isOptional |= (parameter.NullableAnnotation == NullableAnnotation.Annotated || parameter.HasExplicitDefaultValue);
135+
return true;
140136
}
141137

138+
private static string GetParameterName(AttributeData attribute, string parameterName) =>
139+
attribute.TryGetNamedArgumentValue<string>("Name", out var fromSourceName)
140+
? (fromSourceName ?? parameterName)
141+
: parameterName;
142+
142143
public override bool Equals(object obj) =>
143144
obj is EndpointParameter other &&
144145
other.Source == Source &&
145146
other.Name == Name &&
146147
SymbolEqualityComparer.Default.Equals(other.Type, Type);
147148

149+
public bool SignatureEquals(object obj) =>
150+
obj is EndpointParameter other &&
151+
SymbolEqualityComparer.Default.Equals(other.Type, Type);
152+
148153
public override int GetHashCode()
149154
{
150155
var hashCode = new HashCode();

0 commit comments

Comments
 (0)