Skip to content

Commit c6cc85c

Browse files
authored
[RDG] Fix mismatched nullability for structs and HTTP method whitespace (#49805)
1 parent aa76082 commit c6cc85c

File tree

6 files changed

+57
-20
lines changed

6 files changed

+57
-20
lines changed

src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

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

44
using System;
55
using System.Collections.Immutable;
6+
using System.Globalization;
7+
using System.IO;
68
using System.Linq;
79
using System.Text;
810
using Microsoft.CodeAnalysis.CSharp;
@@ -587,13 +589,14 @@ private static bool ShouldUseWith(this JsonTypeInfo jsonTypeInfo, [NotNullWhen(f
587589

588590
public static string GetVerbs(ImmutableHashSet<string> verbs)
589591
{
590-
var builder = new StringBuilder();
592+
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
593+
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 2);
591594

592595
foreach (string verb in verbs.OrderBy(p => p, StringComparer.Ordinal))
593596
{
594-
builder.AppendLine($$"""private static readonly string[] {{verb}}Verb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.{{verb}} };""");
597+
codeWriter.WriteLine($$"""private static readonly string[] {{verb}}Verb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.{{verb}} };""");
595598
}
596599

597-
return builder.ToString();
600+
return stringWriter.ToString();
598601
}
599602
}

src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Model/EndpointParameterExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,25 @@ public static ITypeSymbol UnwrapParameterType(this EndpointParameter parameter)
2626
// If the route handler parameter type is a value type, then we use it as the return type of the BindAsync method.
2727
// Even if the BindAsync method returns a type with mismatched nullability, we need to be able to correctly handle
2828
// mapping nullable value types to non-nullable value types, as in the following example.
29+
//
2930
// public struct NullableStruct
3031
// {
3132
// public static ValueTask<NullableStruct?> BindAsync(HttpContext context, ParameterInfo parameter) {}
3233
// }
3334
// app.MapPost("/", (NullableStruct foo) => { })
3435
if (handlerParameterType.IsValueType)
3536
{
37+
// For generic structs like Struct<T> we want to use the return type of the BindAsync method as the return type
38+
// to avoid issues with mapping the generic type parameter if it contains mismatched nullability.
39+
//
40+
// public struct Struct<T>
41+
// {
42+
// public static ValueTask<Struct<T?>> BindAsync(HttpContext context, ParameterInfo parameter) {}
43+
// }
44+
if (handlerParameterType is INamedTypeSymbol { IsGenericType: true, OriginalDefinition: { SpecialType: not SpecialType.System_Nullable_T } })
45+
{
46+
return bindAsyncReturnType;
47+
}
3648
return handlerParameterType;
3749
}
3850

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ namespace Microsoft.AspNetCore.Http.Generated
5656
file static class GeneratedRouteBuilderExtensionsCore
5757
{
5858
private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get };
59-
private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post };
59+
private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch };
60+
private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post };
61+
private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put };
6062

6163
[InterceptsLocation(@"TestMapActions.cs", 44, 5)]
6264
internal static RouteHandlerBuilder MapGet0(
@@ -191,7 +193,7 @@ private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore
191193
}
192194

193195
[InterceptsLocation(@"TestMapActions.cs", 45, 5)]
194-
internal static RouteHandlerBuilder MapGet1(
196+
internal static RouteHandlerBuilder MapPost1(
195197
this IEndpointRouteBuilder endpoints,
196198
[StringSyntax("Route")] string pattern,
197199
Delegate handler)
@@ -310,13 +312,13 @@ private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore
310312
endpoints,
311313
pattern,
312314
handler,
313-
GetVerb,
315+
PostVerb,
314316
populateMetadata,
315317
createRequestDelegate);
316318
}
317319

318320
[InterceptsLocation(@"TestMapActions.cs", 46, 5)]
319-
internal static RouteHandlerBuilder MapGet2(
321+
internal static RouteHandlerBuilder MapPut2(
320322
this IEndpointRouteBuilder endpoints,
321323
[StringSyntax("Route")] string pattern,
322324
Delegate handler)
@@ -406,13 +408,13 @@ private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore
406408
endpoints,
407409
pattern,
408410
handler,
409-
GetVerb,
411+
PutVerb,
410412
populateMetadata,
411413
createRequestDelegate);
412414
}
413415

414416
[InterceptsLocation(@"TestMapActions.cs", 47, 5)]
415-
internal static RouteHandlerBuilder MapPost3(
417+
internal static RouteHandlerBuilder MapPatch3(
416418
this IEndpointRouteBuilder endpoints,
417419
[StringSyntax("Route")] string pattern,
418420
Delegate handler)
@@ -521,7 +523,7 @@ private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore
521523
endpoints,
522524
pattern,
523525
handler,
524-
PostVerb,
526+
PatchVerb,
525527
populateMetadata,
526528
createRequestDelegate);
527529
}

src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.AsParameters.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,9 @@ static void parametersListWithMetadataType([AsParameters] ParametersListWithMeta
237237
args.HttpContext.Items.Add("value", args.Value);
238238
}
239239
app.MapGet("/parameterListWithDefaultValue/{value}", parameterListWithDefaultValue);
240-
app.MapGet("/parameterListRecordStruct/{value}", parameterListRecordStruct);
241-
app.MapGet("/parametersListWithHttpContext", parametersListWithHttpContext);
242-
app.MapPost("/parametersListWithImplicitFromBody", ([AsParameters] ParametersListWithImplicitFromBody args) => args.Todo.Name ?? string.Empty);
240+
app.MapPost("/parameterListRecordStruct/{value}", parameterListRecordStruct);
241+
app.MapPut("/parametersListWithHttpContext", parametersListWithHttpContext);
242+
app.MapPatch("/parametersListWithImplicitFromBody", ([AsParameters] ParametersListWithImplicitFromBody args) => args.Todo.Name ?? string.Empty);
243243
app.MapGet("/parametersListWithMetadataType", parametersListWithMetadataType);
244244
""");
245245

src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -314,15 +314,19 @@ public async Task BindAsyncRunsBeforeBodyBinding()
314314
public async Task MapAction_BindAsync_MismatchedNullability()
315315
{
316316
var source = """
317-
app.MapGet("/", (BindableWithMismatchedNullability<Todo> param) => "Hello world!");
317+
app.MapGet("/1", (BindableWithMismatchedNullability<Todo> param) => "Hello /1!");
318+
app.MapGet("/2", (BindableStructWithMismatchedNullability<Todo> param) => "Hello /2!");
318319
""";
319320
var (_, compilation) = await RunGeneratorAsync(source);
320-
var endpoint = GetEndpointFromCompilation(compilation);
321-
322-
var httpContext = CreateHttpContext();
323-
324-
await endpoint.RequestDelegate(httpContext);
321+
var endpoints = GetEndpointsFromCompilation(compilation);
325322

326-
await VerifyResponseBodyAsync(httpContext, "Hello world!");
323+
var index = 1;
324+
foreach (var endpoint in endpoints)
325+
{
326+
var httpContext = CreateHttpContext();
327+
await endpoint.RequestDelegate(httpContext);
328+
await VerifyResponseBodyAsync(httpContext, $"Hello /{index}!");
329+
index++;
330+
}
327331
}
328332
}

src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,22 @@ public BindableWithMismatchedNullability(T? value)
10481048
}
10491049
}
10501050

1051+
public struct BindableStructWithMismatchedNullability<T>
1052+
{
1053+
public BindableStructWithMismatchedNullability(T? value)
1054+
{
1055+
Value = value;
1056+
}
1057+
1058+
public T? Value { get; }
1059+
1060+
public static async ValueTask<BindableStructWithMismatchedNullability<T?>> BindAsync(HttpContext httpContext, ParameterInfo parameter)
1061+
{
1062+
await Task.CompletedTask;
1063+
return new BindableStructWithMismatchedNullability<T?>(default);
1064+
}
1065+
}
1066+
10511067
public class BindableClassWithNullReturn
10521068
{
10531069
public static async ValueTask<BindableClassWithNullReturn?> BindAsync(HttpContext httpContext, ParameterInfo parameter)

0 commit comments

Comments
 (0)