aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

EmptyHttpResult is not straightforward to be used in type checking

Open Arclight3 opened this issue 3 years ago • 3 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I created a generic endpoint filter

public class EndpointRequestResponseLoggingFilter<TRequest, TResponse> : IEndpointFilter

which has the purpose to log the request and response objects of an endpoint.

As a side note, I know that .NET 7 offers some similar functionality but it's not good enough for my needs (because it doesn't allow me to mark sensitive properties on the request/response object and filter them).

I made some extension methods to simplify the registration of the endpoint filter:

public static RouteHandlerBuilder AddRequestResponseLoggingFilter<TRequest, TResponse>(this RouteHandlerBuilder builder)
{
    builder.AddEndpointFilter<EndpointRequestResponseLoggingFilter<TRequest, TResponse>>();
    return builder;
}

public static RouteHandlerBuilder AddRequestLoggingFilter<TRequest>(this RouteHandlerBuilder builder)
{
    builder.AddEndpointFilter<EndpointRequestResponseLoggingFilter<TRequest, EmptyHttpResult>>();
    return builder;
}

public static RouteHandlerBuilder AddResponseLoggingFilter<TResponse>(this RouteHandlerBuilder builder)
{
    builder.AddEndpointFilter<EndpointRequestResponseLoggingFilter<EmptyHttpRequest, TResponse>>();
    return builder;
}

and I use them to register the filter on some endpoints:

app.MapPost("/myEndpoint", (MyRequest myRequest) => new MyResponse(1, "Message"))
    .AddRequestResponseLoggingFilter<MyRequest, MyResponse>();

app.MapPost("/myEndpointWithNoResponse", (MyRequest myRequest) => { })
    .AddRequestLoggingFilter<MyRequest>();

app.MapPost("/myEndpointWithNoRequest", () => new MyResponse(1, "abc"))
    .AddResponseLoggingFilter<MyResponse>();

Inside the filter I have this logic:

public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
    // Log request
    if (context.Arguments.Count > 0)
    {
        var request = context.Arguments[0];

        if (request is TRequest)
            // Filter sensitive properties on the request and log the rest of the object
        else if (request is null)
            _logger.LogError("Request: null");
        else
        {
            _logger.LogError("Expected request type '{providedRequestType}' but found type '{actualRequestType}'.", typeof(TRequest).FullName, request.GetType().FullName);
            // Filter sensitive properties on the request and log the rest of the object
        }
    }
    else if (typeof(TRequest) != typeof(EmptyHttpRequest))
        _logger.LogError("Expected request type '{providedRequestType}' but no request object was received.", typeof(TRequest).FullName);
    else
        _logger.LogInformation("Request: No request");

    var response = await next(context);

    // Log response
    if (response is TResponse)
        // Filter sensitive properties on the response and log the rest of the object
    else
    {
        if (response is null)
            _logger.LogError("Response: null");
        else
        {
            if (response.GetType().Name is nameof(EmptyHttpResult))
            {
                if (typeof(TResponse) != typeof(EmptyHttpResult))
                    _logger.LogError("Expected response type '{providedResponseType}' but no response was returned.", typeof(TResponse).FullName);
                else
                    _logger.LogInformation("Response: No response");
            }
           else
            {
                _logger.LogError("Expected response type '{providedResponseType}' but found type '{actualResponseType}'.", typeof(TResponse).FullName, response.GetType().FullName);
                // Filter sensitive properties on the response and log the rest of the object
            }
        }
    }

    return response;
}

As you can see, I am also doing some validations and type checking to make let the developer know that the type he registered on the endpoint filter is not the same as the actual object type.

The EmptyHttpRequest is a record I created to represent the situation when an endpoint doesn't have a request object. The EmptyHttpResult is what I saw that the framework returns on the response when the endpoint has no response (e.g.: "myEndpointWithNoResponse"). So when I have an endpoint with no response I register the filter like this builder.AddEndpointFilter<EndpointRequestResponseLoggingFilter<TRequest, EmptyHttpResult>>()

The problem though is that I have to do the comparison like this: if (response.GetType().Name is nameof(EmptyHttpResult)) because the public typed exposed by the framework is Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult while the type of the response object is Microsoft.AspNetCore.Http.RequestDelegateFactory+EmptyHttpResult.

Describe the solution you'd like

I would like the response object to have the type Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult so I can do the comparison in a much simpler way: if (response is EmptyHttpResult).

Additional context

I am using the dotnet version: 7.0.100

Arclight3 avatar Nov 13 '22 16:11 Arclight3

Agreed, we should fix this and patch 7.0. I think there are maybe 2 high level approaches:

  1. Find a way to move Results.Empty where RequestDelegateFactory is
  2. Use reflection to codegen the call to Results.Empty

davidfowl avatar Nov 13 '22 16:11 davidfowl

Find a way to move Results.Empty where RequestDelegateFactory is

Yeah, we considered this exact same scenario when implementing this. We weren't able to do this the first time around because of the challenges with codesharing identified in https://github.com/dotnet/aspnetcore/issues/41330.

Use reflection to codegen the call to Results.Empty

This might be a fine stopgap...

captainsafia avatar Nov 15 '22 06:11 captainsafia

This is worth patching too.

davidfowl avatar Nov 15 '22 06:11 davidfowl

This is fixed in .NET 7 and 8 now.

captainsafia avatar Mar 27 '23 20:03 captainsafia