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 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
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
5 changes: 5 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
179 changes: 173 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,16 @@ 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, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, 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())
{
logOrThrowExceptionHelper.UnexpectedJsonContentType(httpContext.Request.ContentType);
httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
return (false, default);
}
Expand All @@ -36,17 +38,34 @@ internal static class RequestDelegateGeneratorSources
var bodyValue = await httpContext.Request.ReadFromJsonAsync<T>();
if (!allowEmpty && bodyValue == null)
{
if (!isInferred)
{
logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body");
}
else
{
logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName);
}
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, bodyValue);
}
return (true, bodyValue);
}
catch (IOException)
catch (BadHttpRequestException badHttpRequestException)
{
logOrThrowExceptionHelper.RequestBodyIOException(badHttpRequestException);
httpContext.Response.StatusCode = badHttpRequestException.StatusCode;
return (false, default);
}
catch (System.Text.Json.JsonException)
catch (IOException ioException)
{
logOrThrowExceptionHelper.RequestBodyIOException(ioException);
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, default);
}
catch (System.Text.Json.JsonException jsonException)
{
logOrThrowExceptionHelper.InvalidJsonRequestBody(parameterTypeName, parameterName, jsonException);
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return (false, default);
}
Expand Down Expand Up @@ -96,7 +115,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>(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null)
{
if (serviceProviderIsService is not null)
{
Expand All @@ -105,10 +124,155 @@ 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, logOrThrowExceptionHelper, isOptional, parameterTypeName, parameterName, isInferred: true);
}
""";

public static string LogOrThrowExceptionHelperClass => $$"""
file class LogOrThrowExceptionHelper
{
private readonly ILogger? _rdgLogger;
private readonly bool _shouldThrow;

public LogOrThrowExceptionHelper(IServiceProvider? serviceProvider, RequestDelegateFactoryOptions? options)
{
var loggerFactory = serviceProvider?.GetRequiredService<ILoggerFactory>();
_rdgLogger = loggerFactory?.CreateLogger("{{typeof(RequestDelegateGenerator)}}");
_shouldThrow = options?.ThrowOnBadRequest ?? false;
}

public void RequestBodyIOException(IOException exception)
{
if (_rdgLogger != null)
{
_requestBodyIOException(_rdgLogger, exception);
}
}

private static readonly Action<ILogger, Exception?> _requestBodyIOException =
LoggerMessage.Define(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.RequestBodyIOExceptionEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequestBodyIOExceptionEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequestBodyIOExceptionMessage, true)}});

public void InvalidJsonRequestBody(string parameterTypeName, string parameterName, Exception exception)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyExceptionMessage, true)}}, parameterTypeName, parameterName);
throw new BadHttpRequestException(message, exception);
}

if (_rdgLogger != null)
{
_invalidJsonRequestBody(_rdgLogger, parameterTypeName, parameterName, exception);
}
}

private static readonly Action<ILogger, string, string, Exception?> _invalidJsonRequestBody =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.InvalidJsonRequestBodyEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidJsonRequestBodyLogMessage, true)}});

public void ParameterBindingFailed(string parameterTypeName, string parameterName, string sourceValue)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ParameterBindingFailedExceptionMessage, true)}}, parameterTypeName, parameterName, sourceValue);
throw new BadHttpRequestException(message);
}

if (_rdgLogger != null)
{
_parameterBindingFailed(_rdgLogger, parameterTypeName, parameterName, sourceValue, null);
}
}

private static readonly Action<ILogger, string, string, string, Exception?> _parameterBindingFailed =
LoggerMessage.Define<string, string, string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.ParameterBindingFailedEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ParameterBindingFailedEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ParameterBindingFailedLogMessage, true)}});

public void RequiredParameterNotProvided(string parameterTypeName, string parameterName, string source)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedExceptionMessage, true)}}, parameterTypeName, parameterName, source);
throw new BadHttpRequestException(message);
}

if (_rdgLogger != null)
{
_requiredParameterNotProvided(_rdgLogger, parameterTypeName, parameterName, source, null);
}
}

private static readonly Action<ILogger, string, string, string, Exception?> _requiredParameterNotProvided =
LoggerMessage.Define<string, string, string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.RequiredParameterNotProvidedEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.RequiredParameterNotProvidedLogMessage, true)}});

public void ImplicitBodyNotProvided(string parameterName)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedExceptionMessage, true)}}, parameterName);
throw new BadHttpRequestException(message);
}

if (_rdgLogger != null)
{
_implicitBodyNotProvided(_rdgLogger, parameterName, null);
}
}

private static readonly Action<ILogger, string, Exception?> _implicitBodyNotProvided =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.ImplicitBodyNotProvidedLogMessage, true)}});

public void UnexpectedJsonContentType(string? contentType)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeExceptionMessage, true)}}, contentType);
throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType);
}

if (_rdgLogger != null)
{
_unexpectedJsonContentType(_rdgLogger, contentType ?? "(none)", null);
}
}

private static readonly Action<ILogger, string, Exception?> _unexpectedJsonContentType =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedJsonContentTypeLogMessage, true)}});

public void UnexpectedNonFormContentType(string? contentType)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedFormContentTypeExceptionMessage, true)}}, contentType);
throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType);
}

if (_rdgLogger != null)
{
_unexpectedNonFormContentType(_rdgLogger, contentType ?? "(none)", null);
}
}

private static readonly Action<ILogger, string, Exception?> _unexpectedNonFormContentType =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.UnexpectedFormContentTypeEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedFormContentTypeLogEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.UnexpectedFormContentTypeLogMessage, true)}});

public void InvalidFormRequestBody(string parameterTypeName, string parameterName, Exception exception)
{
if (_shouldThrow)
{
var message = string.Format(CultureInfo.InvariantCulture, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidFormRequestBodyExceptionMessage, true)}}, parameterTypeName, parameterName);
throw new BadHttpRequestException(message, exception);
}

if (_rdgLogger != null)
{
_invalidFormRequestBody(_rdgLogger, parameterTypeName, parameterName, exception);
}
}

private static readonly Action<ILogger, string, string, Exception?> _invalidFormRequestBody =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId({{RequestDelegateCreationLogging.InvalidFormRequestBodyEventId}}, {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidFormRequestBodyEventName, true)}}), {{SymbolDisplay.FormatLiteral(RequestDelegateCreationLogging.InvalidFormRequestBodyLogMessage, true)}});
}
""";

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

Expand Down Expand Up @@ -153,6 +317,7 @@ 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;

Expand Down Expand Up @@ -199,6 +364,8 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext)

{{helperMethods}}
}

{{LogOrThrowExceptionHelperClass}}
}
""";
private static string GetGenericThunks(string genericThunks) => genericThunks != string.Empty ? $$"""
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)}>(logOrThrowExceptionHelper, {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 logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options);");
}
}

public static string EmitArgumentList(this Endpoint endpoint) => string.Join(", ", endpoint.Parameters.Select(p => p.EmitArgument()));
}
Loading