Skip to content

Add support for logging/throwing exceptions in RDG #47657

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 5 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)HashCode.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RequestDelegateCreationMessages.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\BoundedCacheWithFactory.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypeData.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypes.cs" LinkBase="Shared" />
Expand Down
11 changes: 11 additions & 0 deletions src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
codeWriter.StartBlock();
codeWriter.WriteLine($"var handler = ({endpoint!.EmitHandlerDelegateType(considerOptionality: true)})del;");
codeWriter.WriteLine("EndpointFilterDelegate? filteredInvocation = null;");
if (endpoint!.EmitterContext.RequiresLoggingHelper || endpoint!.EmitterContext.HasJsonBodyOrService || endpoint!.Response?.IsSerializableJsonResponse(out var _) is true)
{
codeWriter.WriteLine("var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;");
}
endpoint!.EmitLoggingPreamble(codeWriter);
endpoint!.EmitRouteOrQueryResolver(codeWriter);
endpoint!.EmitJsonBodyOrServiceResolver(codeWriter);
endpoint!.Response?.EmitJsonPreparation(codeWriter);
Expand Down Expand Up @@ -164,6 +169,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var hasBindAsync = endpoints.Any(endpoint => endpoint!.EmitterContext.HasBindAsync);
var hasParsable = endpoints.Any(endpoint => endpoint!.EmitterContext.HasParsable);
var hasJsonResponse = endpoints.Any(endpoint => endpoint!.EmitterContext.HasJsonResponse);
var requiredLoggingHelper = endpoints.Any(endpoint => endpoint!.EmitterContext.RequiresLoggingHelper);

using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);
Expand Down Expand Up @@ -198,6 +204,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
codeWriter.WriteLine(RequestDelegateGeneratorSources.TryParseExplicitMethod);
}

if (requiredLoggingHelper)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.LogOrThrowExceptionMethod);
}

return stringWriter.ToString();
});

Expand Down
51 changes: 45 additions & 6 deletions src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator;

internal static class RequestDelegateGeneratorSources
Expand All @@ -19,15 +20,17 @@ internal static class RequestDelegateGeneratorSources

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

public static string TryResolveBodyAsyncMethod => """
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty)
public static string TryResolveBodyAsyncMethod => $$"""
private static async ValueTask<(bool, T?)> TryResolveBodyAsync<T>(HttpContext httpContext, bool allowEmpty, LogOrThrowExceptionAction logOrThrowException, string parameterTypeName, string parameterName, bool isInferred = false)
{
var feature = httpContext.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpRequestBodyDetectionFeature>();

if (feature?.CanHaveBody == true)
{
if (!httpContext.Request.HasJsonContentType())
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeExceptionMessage, true)}}, httpContext.Request.ContentType);
logOrThrowException({{RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventId}}, message, null, StatusCodes.Status415UnsupportedMediaType);
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
return (false, default);
}
Expand All @@ -36,17 +39,30 @@ internal static class RequestDelegateGeneratorSources
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
if (!allowEmpty && bodyValue == null)
{
if (!isInferred)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedExceptionMessage, true)}}, parameterTypeName, parameterName, "body");
logOrThrowException({{RequestDelegateCreationLogging.RequiredParameterNotProvidedEventId}}, message, null);
}
else
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedExceptionMessage, true)}}, parameterName);
logOrThrowException({{RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventId}}, message, null);
}
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, bodyValue);
}
return (true, bodyValue);
}
catch (IOException)
catch (IOException ioException)
{
logOrThrowException({{RequestDelegateCreationLogging.RequestBodyIOExceptionEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequestBodyIOExceptionMessage, true)}}, ioException, StatusCodes.Status500InternalServerError);
return (false, default);
}
catch (System.Text.Json.JsonException)
catch (System.Text.Json.JsonException jsonException)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyExceptionMessage, true)}}, parameterTypeName, parameterName);
logOrThrowException({{RequestDelegateCreationLogging.InvalidJsonRequestBodyEventId}}, message, jsonException, StatusCodes.Status400BadRequest);
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, default);
}
Expand Down Expand Up @@ -96,7 +112,7 @@ private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, J
""";

public static string ResolveJsonBodyOrServiceMethod => """
private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(IServiceProviderIsService? serviceProviderIsService = null)
private static Func<HttpContext, bool, ValueTask<(bool, T?)>> ResolveJsonBodyOrService<T>(LogOrThrowExceptionAction logOrThrowException, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null)
{
if (serviceProviderIsService is not null)
{
Expand All @@ -105,7 +121,28 @@ private static Task WriteToResponseAsync<T>(T? value, HttpContext httpContext, J
return static (httpContext, isOptional) => new ValueTask<(bool, T?)>((true, httpContext.RequestServices.GetService<T>()));
}
}
return static (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional);
return (httpContext, isOptional) => TryResolveBodyAsync<T>(httpContext, isOptional, logOrThrowException, parameterTypeName, parameterName, isInferred: true);
}
""";

public static string LogOrThrowExceptionMethod => $$"""
private static LogOrThrowExceptionAction GetLogOrThrowException(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options)
{
var loggerFactory = serviceProvider?.GetRequiredService<ILoggerFactory>();
var rdgLogger = loggerFactory?.CreateLogger("{{typeof(RequestDelegateGenerator)}}");
void logOrThrowException(int eventId, string message, Exception? exception = null, int statusCode = StatusCodes.Status400BadRequest)
{
if ((options?.ThrowOnBadRequest ?? false) && eventId != {{RequestDelegateCreationLogging.RequestBodyIOExceptionEventId}})
{
if (exception != null)
{
throw new BadHttpRequestException(message, statusCode, exception);
}
throw new BadHttpRequestException(message, statusCode);
}
rdgLogger?.LogDebug(eventId, exception, message);
}
return logOrThrowException;
}
""";

Expand Down Expand Up @@ -153,11 +190,13 @@ namespace Microsoft.AspNetCore.Http.Generated
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.Options;

using MetadataPopulator = System.Func<System.Reflection.MethodInfo, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions?, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult>;
using RequestDelegateFactoryFunc = System.Func<System.Delegate, Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions, Microsoft.AspNetCore.Http.RequestDelegateMetadataResult?, Microsoft.AspNetCore.Http.RequestDelegateResult>;
delegate void LogOrThrowExceptionAction(int eventId, string message, Exception? exceptipon = null, int statusCode = StatusCodes.Status400BadRequest);

file static class GeneratedRouteBuilderExtensionsCore
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ internal sealed class EmitterContext
public bool HasBindAsync { get; set; }
public bool HasParsable { get; set; }
public bool HasJsonResponse { get; set; }
public bool RequiresLoggingHelper { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;

internal static class EmitterExtensions
{
public static string ToMessageString(this EndpointParameter endpointParameter) => endpointParameter.Source switch
{
EndpointParameterSource.Header => "header",
EndpointParameterSource.Query => "query string",
EndpointParameterSource.RouteOrQuery => "route or query string",
EndpointParameterSource.BindAsync => endpointParameter.BindMethod == BindabilityMethod.BindAsync
? $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext)"
: $"{endpointParameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)}.BindAsync(HttpContext, ParameterInfo)",
_ => "unknown"
};

public static bool IsSerializableJsonResponse(this EndpointResponse endpointResponse, [NotNullWhen(true)] out ITypeSymbol? responseTypeSymbol)
{
responseTypeSymbol = null;
if (endpointResponse is { IsSerializable: true, ResponseType: { } responseType })
{
responseTypeSymbol = responseType;
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Globalization;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -70,14 +72,23 @@ public static void EmitJsonBodyOrServiceResolver(this Endpoint endpoint, CodeWri
{
if (!serviceProviderEmitted)
{
codeWriter.WriteLine("var serviceProviderIsService = options?.ServiceProvider?.GetService<IServiceProviderIsService>();");
codeWriter.WriteLine("var serviceProviderIsService = serviceProvider?.GetService<IServiceProviderIsService>();");
serviceProviderEmitted = true;
}
codeWriter.Write($@"var {parameter.SymbolName}_JsonBodyOrServiceResolver = ");
codeWriter.WriteLine($"ResolveJsonBodyOrService<{parameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(serviceProviderIsService);");
var shortParameterTypeName = parameter.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat);
codeWriter.WriteLine($"ResolveJsonBodyOrService<{parameter.Type.ToDisplayString(EmitterConstants.DisplayFormat)}>(logOrThrowException, {SymbolDisplay.FormatLiteral(shortParameterTypeName, true)}, {SymbolDisplay.FormatLiteral(parameter.SymbolName, true)}, serviceProviderIsService);");
}
}
}

public static void EmitLoggingPreamble(this Endpoint endpoint, CodeWriter codeWriter)
{
if (endpoint.EmitterContext.RequiresLoggingHelper)
{
codeWriter.WriteLine("var logOrThrowException = GetLogOrThrowException(serviceProvider, options);");
}
}

public static string EmitArgumentList(this Endpoint endpoint) => string.Join(", ", endpoint.Parameters.Select(p => p.EmitArgument()));
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ internal static class EndpointJsonResponseEmitter
{
internal static void EmitJsonPreparation(this EndpointResponse endpointResponse, CodeWriter codeWriter)
{
if (endpointResponse is { IsSerializable: true, ResponseType: {} responseType })
if (endpointResponse.IsSerializableJsonResponse(out var responseType))
{
var typeName = responseType.ToDisplayString(EmitterConstants.DisplayFormat);

codeWriter.WriteLine("var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices;");
codeWriter.WriteLine("var serializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? new JsonOptions().SerializerOptions;");
codeWriter.WriteLine($"var jsonTypeInfo = (JsonTypeInfo<{typeName}>)serializerOptions.GetTypeInfo(typeof({typeName}));");
}
Expand Down
Loading