Skip to content

Commit 1a70e36

Browse files
authored
First pass at basic query string parameters. (#46545)
* First pass at basic string parameters.
1 parent d6fa351 commit 1a70e36

25 files changed

+1386
-75
lines changed

src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
<Compile Include="$(SharedSourceRoot)Nullable\NullableAttributes.cs" />
2626
<Compile Include="$(SharedSourceRoot)RoslynUtils\BoundedCacheWithFactory.cs" LinkBase="Shared" />
2727
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypes.cs" LinkBase="Shared" />
28+
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypeData.cs" LinkBase="Shared" />
29+
<Compile Include="$(SharedSourceRoot)RoslynUtils\SymbolExtensions.cs" LinkBase="Shared" />
2830
</ItemGroup>
2931

3032
</Project>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
<Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
2222
<Compile Include="$(SharedSourceRoot)HashCode.cs" LinkBase="Shared" />
2323
<Compile Include="$(SharedSourceRoot)RoslynUtils\BoundedCacheWithFactory.cs" LinkBase="Shared" />
24+
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypeData.cs" LinkBase="Shared" />
2425
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypes.cs" LinkBase="Shared" />
26+
<Compile Include="$(SharedSourceRoot)RoslynUtils\SymbolExtensions.cs" LinkBase="Shared" />
2527
</ItemGroup>
2628

2729
</Project>

src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
120120
lineNumber);
121121
}
122122
""");
123-
}
123+
}
124124

125125
return code.ToString();
126126
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
8+
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
9+
internal static class EndpointEmitter
10+
{
11+
internal static string EmitParameterPreparation(this Endpoint endpoint)
12+
{
13+
var parameterPreparationBuilder = new StringBuilder();
14+
15+
for (var parameterIndex = 0; parameterIndex < endpoint.Parameters.Length; parameterIndex++)
16+
{
17+
var parameter = endpoint.Parameters[parameterIndex];
18+
19+
var parameterPreparationCode = parameter switch
20+
{
21+
{
22+
Source: EndpointParameterSource.SpecialType
23+
} => parameter.EmitSpecialParameterPreparation(),
24+
{
25+
Source: EndpointParameterSource.Query,
26+
} => parameter.EmitQueryParameterPreparation(),
27+
_ => throw new Exception("Unreachable!")
28+
};
29+
30+
// To avoid having two newlines after the block of parameter handling code.
31+
if (parameterIndex < endpoint.Parameters.Length - 1)
32+
{
33+
parameterPreparationBuilder.AppendLine(parameterPreparationCode);
34+
}
35+
else
36+
{
37+
parameterPreparationBuilder.Append(parameterPreparationCode);
38+
}
39+
}
40+
41+
return parameterPreparationBuilder.ToString();
42+
}
43+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
8+
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
9+
internal static class EndpointParameterEmitter
10+
{
11+
internal static string EmitSpecialParameterPreparation(this EndpointParameter endpointParameter)
12+
{
13+
return $"""
14+
var {endpointParameter.Name}_local = {endpointParameter.AssigningCode};
15+
""";
16+
}
17+
18+
internal static string EmitQueryParameterPreparation(this EndpointParameter endpointParameter)
19+
{
20+
var builder = new StringBuilder();
21+
22+
// Preamble for diagnostics purposes.
23+
builder.AppendLine($$"""
24+
// Endpoint Parameter: {{endpointParameter.Name}} (Type = {{endpointParameter.Type}}, IsOptional = {{endpointParameter.IsOptional}}, Source = {{endpointParameter.Source}})
25+
""");
26+
27+
// Grab raw input from HttpContext.
28+
builder.AppendLine($$"""
29+
var {{endpointParameter.Name}}_raw = {{endpointParameter.AssigningCode}};
30+
""");
31+
32+
// If we are not optional, then at this point we can just assign the string value to the handler argument,
33+
// otherwise we need to detect whether no value is provided and set the handler argument to null to
34+
// preserve consistency with RDF behavior. We don't want to emit the conditional block to avoid
35+
// compiler errors around null handling.
36+
if (endpointParameter.IsOptional)
37+
{
38+
builder.AppendLine($$"""
39+
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.Count > 0 ? {{endpointParameter.Name}}_raw.ToString() : null;
40+
""");
41+
}
42+
else
43+
{
44+
builder.AppendLine($$"""
45+
if (StringValues.IsNullOrEmpty({{endpointParameter.Name}}_raw))
46+
{
47+
httpContext.Response.StatusCode = 400;
48+
return Task.CompletedTask;
49+
}
50+
var {{endpointParameter.HandlerArgument}} = {{endpointParameter.Name}}_raw.ToString();
51+
""");
52+
}
53+
54+
return builder.ToString();
55+
}
56+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public static bool SignatureEquals(Endpoint a, Endpoint b)
8888

8989
for (var i = 0; i < a.Parameters.Length; i++)
9090
{
91-
if (a.Parameters[i].Equals(b.Parameters[i]))
91+
if (!a.Parameters[i].Equals(b.Parameters[i]))
9292
{
9393
return false;
9494
}

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

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
6+
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
67
using Microsoft.CodeAnalysis;
78
using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
810

911
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
1012

@@ -15,11 +17,25 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
1517
Type = parameter.Type;
1618
Name = parameter.Name;
1719
Source = EndpointParameterSource.Unknown;
20+
HandlerArgument = $"{parameter.Name}_local";
1821

19-
if (GetSpecialTypeCallingCode(Type, wellKnownTypes) is string callingCode)
22+
var fromQueryMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata);
23+
24+
if (GetSpecialTypeAssigningCode(Type, wellKnownTypes) is string assigningCode)
2025
{
2126
Source = EndpointParameterSource.SpecialType;
22-
CallingCode = callingCode;
27+
AssigningCode = assigningCode;
28+
}
29+
else if (parameter.HasAttributeImplementingInterface(fromQueryMetadataInterfaceType))
30+
{
31+
Source = EndpointParameterSource.Query;
32+
AssigningCode = $"httpContext.Request.Query[\"{parameter.Name}\"]";
33+
IsOptional = parameter.Type is INamedTypeSymbol parameterType && parameterType.NullableAnnotation == NullableAnnotation.Annotated;
34+
}
35+
else
36+
{
37+
// TODO: Inferencing rules go here - but for now:
38+
Source = EndpointParameterSource.Unknown;
2339
}
2440
}
2541

@@ -28,23 +44,17 @@ public EndpointParameter(IParameterSymbol parameter, WellKnownTypes wellKnownTyp
2844

2945
// TODO: If the parameter has [FromRoute("AnotherName")] or similar, prefer that.
3046
public string Name { get; }
31-
public string? CallingCode { get; }
47+
public string? AssigningCode { get; }
48+
public string HandlerArgument { get; }
49+
public bool IsOptional { get; }
3250

3351
public string EmitArgument()
3452
{
35-
switch (Source)
36-
{
37-
case EndpointParameterSource.SpecialType:
38-
return CallingCode!;
39-
default:
40-
// Eventually there should be know unknown parameter sources, but in the meantime we don't expect them to get this far.
41-
// The netstandard2.0 target means there is no UnreachableException.
42-
throw new Exception("Unreachable!");
43-
}
53+
return HandlerArgument;
4454
}
4555

4656
// TODO: Handle special form types like IFormFileCollection that need special body-reading logic.
47-
private static string? GetSpecialTypeCallingCode(ITypeSymbol type, WellKnownTypes wellKnownTypes)
57+
private static string? GetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTypes wellKnownTypes)
4858
{
4959
if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_HttpContext)))
5060
{

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

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

44
using System;
5+
using System.Collections.Generic;
56
using System.Linq;
7+
using System.Net.Cache;
68
using System.Text;
9+
using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
710
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
812

913
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
1014

@@ -61,14 +65,18 @@ public static string EmitRequestHandler(this Endpoint endpoint)
6165
var resultAssignment = endpoint.Response.IsVoid ? string.Empty : "var result = ";
6266
var awaitHandler = endpoint.Response.IsAwaitable ? "await " : string.Empty;
6367
var setContentType = endpoint.Response.IsVoid ? string.Empty : $@"httpContext.Response.ContentType ??= ""{endpoint.Response.ContentType}"";";
64-
return $$"""
68+
69+
var requestHandlerSource = $$"""
6570
{{handlerSignature}}
6671
{
72+
{{endpoint.EmitParameterPreparation()}}
6773
{{setContentType}}
6874
{{resultAssignment}}{{awaitHandler}}handler({{endpoint.EmitArgumentList()}});
6975
{{(endpoint.Response.IsVoid ? "return Task.CompletedTask;" : endpoint.EmitResponseWritingCall())}}
7076
}
7177
""";
78+
79+
return requestHandlerSource;
7280
}
7381

7482
private static string EmitResponseWritingCall(this Endpoint endpoint)
@@ -108,14 +116,12 @@ private static string EmitResponseWritingCall(this Endpoint endpoint)
108116
* can be used to reduce the boxing that happens at runtime when constructing
109117
* the context object.
110118
*/
111-
public static string EmitFilteredRequestHandler(this Endpoint endpoint)
119+
public static string EmitFilteredRequestHandler(this Endpoint _)
112120
{
113-
var argumentList = endpoint.Parameters.Length == 0 ? string.Empty : $", {endpoint.EmitArgumentList()}";
114-
115121
return $$"""
116122
async Task RequestHandlerFiltered(HttpContext httpContext)
117123
{
118-
var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext{{argumentList}}));
124+
var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext));
119125
await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);
120126
}
121127
""";

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

Lines changed: 0 additions & 45 deletions
This file was deleted.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Reference Include="Microsoft.AspNetCore.Http" />
1717
<Reference Include="Microsoft.AspNetCore.Http.Results" />
1818
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
19+
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
1920
<Reference Include="Microsoft.CodeAnalysis.CSharp" />
2021
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
2122
<Reference Include="Microsoft.Extensions.DependencyInjection" />

src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.AspNetCore.Http.Json;
1212
using System.Text.Json.Serialization;
1313
using Microsoft.CodeAnalysis;
14+
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
1415

1516
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
1617

src/Http/Http.Extensions/test/ProblemDetailsServiceCollectionExtensionsTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Extensions.DependencyInjection.Extensions;
99
using Microsoft.Extensions.Options;
1010
using Moq;
11+
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
1112

1213
namespace Microsoft.AspNetCore.Http.Extensions.Tests;
1314

0 commit comments

Comments
 (0)