Skip to content

Follow up on IParsable changes and update tests #47102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
codeWriter.WriteLineNoTabs(string.Empty);
codeWriter.WriteLine("if (options?.EndpointBuilder?.FilterFactories.Count > 0)");
codeWriter.StartBlock();
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>");
if (endpoint.Response.IsAwaitable)
{
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(async ic =>");
}
else
{
codeWriter.WriteLine("filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic =>");
}
codeWriter.StartBlock();
codeWriter.WriteLine("if (ic.HttpContext.Response.StatusCode == 400)");
codeWriter.StartBlock();
codeWriter.WriteLine("return ValueTask.FromResult<object?>(Results.Empty);");
codeWriter.EndBlock();
codeWriter.WriteLine(endpoint.EmitFilteredInvocation());
endpoint.EmitFilteredInvocation(codeWriter);
codeWriter.EndBlockWithComma();
codeWriter.WriteLine("options.EndpointBuilder,");
codeWriter.WriteLine("handler.Method);");
Expand Down Expand Up @@ -146,11 +153,58 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return stringWriter.ToString();
});

var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions);
var endpointHelpers = endpoints
.Collect()
.Select((endpoints, _) =>
{
var hasJsonBodyOrService = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonBodyOrService);
var hasJsonBody = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonBody);
var hasRouteOrQuery = endpoints.Any(endpoint => endpoint.EmitterContext.HasRouteOrQuery);
var hasBindAsync = endpoints.Any(endpoint => endpoint.EmitterContext.HasBindAsync);
var hasParsable = endpoints.Any(endpoint => endpoint.EmitterContext.HasParsable);
var hasJsonResponse = endpoints.Any(endpoint => endpoint.EmitterContext.HasJsonResponse);

using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);

if (hasRouteOrQuery)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.ResolveFromRouteOrQueryMethod);
}

if (hasJsonResponse)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.WriteToResponseAsyncMethod);
}

if (hasJsonBody || hasJsonBodyOrService)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveBodyAsyncMethod);
}

if (hasBindAsync)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.BindAsyncMethod);
}

if (hasJsonBodyOrService)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryResolveJsonBodyOrServiceAsyncMethod);
}

if (hasParsable)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryParseExplicitMethod);
}

return stringWriter.ToString();
});

var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions).Combine(endpointHelpers);

context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) =>
{
var (thunks, endpointsCode) = sources;
var ((thunks, endpointsCode), helpers) = sources;

if (thunks.IsDefaultOrEmpty || string.IsNullOrEmpty(endpointsCode))
{
Expand All @@ -166,7 +220,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource(
genericThunks: string.Empty,
thunks: thunksCode.ToString(),
endpoints: endpointsCode);
endpoints: endpointsCode,
helperMethods: helpers ?? string.Empty);

context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code);
});
Expand Down
174 changes: 92 additions & 82 deletions src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,97 @@ internal static class RequestDelegateGeneratorSources

public static string GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]";

public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$"""
public static string TryResolveBodyAsyncMethod => """
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these strings be cut over to CodeWriter implementations. Given where these get emitted, we can pretty easily predict the indentation requirements but really just asking for the sake of consistency since they are multi-line blocks.

{
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();

if (feature?.CanHaveBody == true)
{
if (!httpContext.Request.HasJsonContentType())
{
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
return (false, default);
}
try
{
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
if (!allowEmpty && bodyValue == null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, bodyValue);
}
return (true, bodyValue);
}
catch (IOException)
{
return (false, default);
}
catch (System.Text.Json.JsonException)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, default);
}
}
else if (!allowEmpty)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}

return (false, default);
}
""";

public static string TryParseExplicitMethod => """
private static bool TryParseExplicit<T>(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) where T: IParsable<T>
=> T.TryParse(s, provider, out result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😲 does RDG now support something RDF doesn't?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this was part of the original TryParse support in RDG. IMO, we shouldn't not do something in the RDG just because the RDF doesn't support it yet...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should file an issue to support it on both.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

""";

public static string TryResolveJsonBodyOrServiceAsyncMethod => """
private static ValueTask<(bool, T?)> TryResolveJsonBodyOrServiceAsync<T>(HttpContext httpContext, bool isOptional, IServiceProviderIsService? serviceProviderIsService = null)
{
if (serviceProviderIsService is not null)
{
if (serviceProviderIsService.IsService(typeof(T)))
{
return new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
}
}
return TryResolveBodyAsync<T>(httpContext, isOptional);
}
""";

public static string BindAsyncMethod => """
private static ValueTask<T?> BindAsync<T>(HttpContext context, ParameterInfo parameter)
where T : class, IBindableFromHttpContext<T>
{
return T.BindAsync(context, parameter);
}
""";

public static string ResolveFromRouteOrQueryMethod => """
private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
{
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
: (httpContext) => httpContext.Request.Query[parameterName];
}
""";

public static string WriteToResponseAsyncMethod => """
private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo, JsonSerializerOptions options)
{
var runtimeType = value?.GetType();
if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.PolymorphismOptions is not null)
{
return httpContext.Response.WriteAsJsonAsync(value!, jsonTypeInfo);
}

return httpContext.Response.WriteAsJsonAsync(value!, options.GetTypeInfo(runtimeType));
}
""";

public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints, string helperMethods) => $$"""
{{SourceHeader}}

namespace Microsoft.AspNetCore.Builder
Expand Down Expand Up @@ -107,87 +197,7 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext)
}
}

private static Func<HttpContext, StringValues> ResolveFromRouteOrQuery(string parameterName, IEnumerable<string>? routeParameterNames)
{
return routeParameterNames?.Contains(parameterName, StringComparer.OrdinalIgnoreCase) == true
? (httpContext) => new StringValues((string?)httpContext.Request.RouteValues[parameterName])
: (httpContext) => httpContext.Request.Query[parameterName];
}

private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, JsonTypeInfo<T> jsonTypeInfo, JsonSerializerOptions options)
{
var runtimeType = value?.GetType();
if (runtimeType is null || jsonTypeInfo.Type == runtimeType || jsonTypeInfo.PolymorphismOptions is not null)
{
return httpContext.Response.WriteAsJsonAsync(value!, jsonTypeInfo);
}

return httpContext.Response.WriteAsJsonAsync(value!, options.GetTypeInfo(runtimeType));
}

private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
{
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();

if (feature?.CanHaveBody == true)
{
if (!httpContext.Request.HasJsonContentType())
{
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
return (false, default);
}
try
{
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
if (!allowEmpty && bodyValue == null)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, bodyValue);
}
return (true, bodyValue);
}
catch (IOException)
{
return (false, default);
}
catch (System.Text.Json.JsonException)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, default);
}
}
else if (!allowEmpty)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}

return (false, default);
}

private static ValueTask<T?> BindAsync<T>(HttpContext context, ParameterInfo parameter)
where T : class, IBindableFromHttpContext<T>
{
return T.BindAsync(context, parameter);
}

private static ValueTask<(bool, T?)> TryResolveJsonBodyOrServiceAsync<T>(HttpContext httpContext, bool isOptional, IServiceProviderIsService? serviceProviderIsService = null)
{
if (serviceProviderIsService is not null)
{
if (serviceProviderIsService.IsService(typeof(T)))
{
return new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
}
}
return TryResolveBodyAsync<T>(httpContext, isOptional);
}
}

{{GeneratedCodeAttribute}}
file static class ParsableHelper<T> where T : IParsable<T>
{
public static T Parse(string s, IFormatProvider? provider) => T.Parse(s, provider);
public static bool TryParse(string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out T result) => T.TryParse(s, provider, out result);
{{helperMethods}}
}
}
""";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;

internal sealed class EmitterContext
{
public bool HasJsonBodyOrService { get; set; }
public bool HasJsonBody { get; set; }
public bool HasRouteOrQuery { get; set; }
public bool HasBindAsync { get; set; }
public bool HasParsable { get; set; }
public bool HasJsonResponse { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;

namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;

internal static class EndpointJsonResponseEmitter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,14 @@ internal static void EmitJsonBodyParameterPreparationString(this EndpointParamet
// Invoke TryResolveBody method to parse JSON and set
// status codes on exceptions.
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveBodyAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")})";
codeWriter.WriteLine($"var (isSuccessful, {endpointParameter.EmitHandlerArgument()}) = {assigningCode};");
var resolveBodyResult = $"{endpointParameter.Name}_resolveBodyResult";
codeWriter.WriteLine($"var {resolveBodyResult} = {assigningCode};");
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {resolveBodyResult}.Item2;");

// If binding from the JSON body fails, we exit early. Don't
// set the status code here because assume it has been set by the
// TryResolveBody method.
codeWriter.WriteLine("if (!isSuccessful)");
codeWriter.WriteLine($"if (!{resolveBodyResult}.Item1)");
codeWriter.StartBlock();
codeWriter.WriteLine("return;");
codeWriter.EndBlock();
Expand All @@ -128,11 +130,13 @@ internal static void EmitJsonBodyOrServiceParameterPreparationString(this Endpoi
// type from DI if it exists. Otherwise, resolve the parameter
// as a body parameter.
var assigningCode = $"await GeneratedRouteBuilderExtensionsCore.TryResolveJsonBodyOrServiceAsync<{endpointParameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(httpContext, {(endpointParameter.IsOptional ? "true" : "false")}, serviceProviderIsService)";
codeWriter.WriteLine($"var (isSuccessful, {endpointParameter.EmitHandlerArgument()}) = {assigningCode};");
var resolveJsonBodyOrServiceResult = $"{endpointParameter.Name}_resolveJsonBodyOrServiceResult";
codeWriter.WriteLine($"var {resolveJsonBodyOrServiceResult} = {assigningCode};");
codeWriter.WriteLine($"var {endpointParameter.EmitHandlerArgument()} = {resolveJsonBodyOrServiceResult}.Item2;");

// If binding from the JSON body fails, TryResolveJsonBodyOrService
// will return `false` and we will need to exit early.
codeWriter.WriteLine("if (!isSuccessful)");
codeWriter.WriteLine($"if (!{resolveJsonBodyOrServiceResult}.Item1)");
codeWriter.StartBlock();
codeWriter.WriteLine("return;");
codeWriter.EndBlock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel.Emitters;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
Expand All @@ -18,6 +20,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
Operation = operation;
Location = GetLocation(operation);
HttpMethod = GetHttpMethod(operation);
EmitterContext = new EmitterContext();

if (!operation.TryGetRouteHandlerPattern(out var routeToken))
{
Expand All @@ -34,6 +37,7 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
}

Response = new EndpointResponse(method, wellKnownTypes);
EmitterContext.HasJsonResponse = !(Response.ResponseType.IsSealed || Response.ResponseType.IsValueType);
IsAwaitable = Response.IsAwaitable;

if (method.Parameters.Length == 0)
Expand Down Expand Up @@ -75,12 +79,19 @@ public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
}

Parameters = parameters;

EmitterContext.HasJsonBodyOrService = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBodyOrService);
EmitterContext.HasJsonBody = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.JsonBody);
EmitterContext.HasRouteOrQuery = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.RouteOrQuery);
EmitterContext.HasBindAsync = Parameters.Any(parameter => parameter.Source == EndpointParameterSource.BindAsync);
EmitterContext.HasParsable = Parameters.Any(parameter => parameter.IsParsable);
}

public string HttpMethod { get; }
public bool IsAwaitable { get; }
public bool NeedsParameterArray { get; }
public string? RoutePattern { get; }
public EmitterContext EmitterContext { get; }
public EndpointResponse? Response { get; }
public EndpointParameter[] Parameters { get; } = Array.Empty<EndpointParameter>();
public List<Diagnostic> Diagnostics { get; } = new List<Diagnostic>();
Expand Down
Loading