Skip to content

Get EmptyHttpResult in RDF via reflection (#45878) #46048

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 2 commits into from
Jan 12, 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
43 changes: 19 additions & 24 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,17 @@ public static partial class RequestDelegateFactory
private static readonly MemberExpression FormFilesExpr = Expression.Property(FormExpr, typeof(IFormCollection).GetProperty(nameof(IFormCollection.Files))!);
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
private static readonly NewExpression EmptyHttpResultValueTaskExpr = Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(EmptyHttpResult) })!, Expression.Property(null, typeof(EmptyHttpResult), nameof(EmptyHttpResult.Instance)));

// Due to https://github.com/dotnet/aspnetcore/issues/41330 we cannot reference the EmptyHttpResult type
// but users still need to assert on it as in https://github.com/dotnet/aspnetcore/issues/45063
// so we temporarily work around this here by using reflection to get the actual type.
private static readonly object? EmptyHttpResultInstance = Type.GetType("Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult, Microsoft.AspNetCore.Http.Results")?.GetProperty("Instance")?.GetValue(null, null);
#if DEBUG
private static readonly NewExpression EmptyHttpResultValueTaskExpr = EmptyHttpResultInstance is not null
? Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(IResult) })!, Expression.Constant(EmptyHttpResultInstance))
: throw new UnreachableException("The EmptyHttpResult type could not be found.");
#else
private static readonly NewExpression EmptyHttpResultValueTaskExpr = Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(IResult) })!, Expression.Constant(EmptyHttpResultInstance));
#endif
private static readonly ParameterExpression TempSourceStringExpr = ParameterBindingMethodCache.TempSourceStringExpr;
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
Expand Down Expand Up @@ -389,6 +398,7 @@ private static Expression[] CreateArgumentsAndInferMetadata(MethodInfo methodInf
private static EndpointFilterDelegate? CreateFilterPipeline(MethodInfo methodInfo, Expression? targetExpression, RequestDelegateFactoryContext factoryContext, Expression<Func<HttpContext, object?>>? targetFactory)
{
Debug.Assert(factoryContext.EndpointBuilder.FilterFactories.Count > 0);
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
// httpContext.Response.StatusCode >= 400
// ? Task.CompletedTask
// : {
Expand Down Expand Up @@ -453,6 +463,7 @@ targetExpression is null

private static Expression MapHandlerReturnTypeToValueTask(Expression methodCall, Type returnType)
{
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
if (returnType == typeof(void))
{
return Expression.Block(methodCall, EmptyHttpResultValueTaskExpr);
Expand Down Expand Up @@ -2097,32 +2108,34 @@ static async Task ExecuteAwaited(ValueTask task)

private static ValueTask<object?> ExecuteTaskWithEmptyResult(Task task)
{
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
static async ValueTask<object?> ExecuteAwaited(Task task)
{
await task;
return EmptyHttpResult.Instance;
return EmptyHttpResultInstance;
}

if (task.IsCompletedSuccessfully)
{
return new ValueTask<object?>(EmptyHttpResult.Instance);
return new ValueTask<object?>(EmptyHttpResultInstance);
}

return ExecuteAwaited(task);
}

private static ValueTask<object?> ExecuteValueTaskWithEmptyResult(ValueTask valueTask)
{
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
static async ValueTask<object?> ExecuteAwaited(ValueTask task)
{
await task;
return EmptyHttpResult.Instance;
return EmptyHttpResultInstance;
}

if (valueTask.IsCompletedSuccessfully)
{
valueTask.GetAwaiter().GetResult();
return new ValueTask<object?>(EmptyHttpResult.Instance);
return new ValueTask<object?>(EmptyHttpResultInstance);
}

return ExecuteAwaited(valueTask);
Expand Down Expand Up @@ -2442,24 +2455,6 @@ private static void FormatTrackedParameters(RequestDelegateFactoryContext factor
}
}

// Due to cyclic references between Http.Extensions and
// Http.Results, we define our own instance of the `EmptyHttpResult`
// type here.
private sealed class EmptyHttpResult : IResult
{
private EmptyHttpResult()
{
}

public static EmptyHttpResult Instance { get; } = new();

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
{
return Task.CompletedTask;
}
}

private sealed class RDFEndpointBuilder : EndpointBuilder
{
public RDFEndpointBuilder(IServiceProvider applicationServices)
Expand Down
42 changes: 41 additions & 1 deletion src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -97,7 +98,18 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate)
{
var httpContext = CreateHttpContext();

var factoryResult = RequestDelegateFactory.Create(@delegate);
var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions()
{
EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>()
{
(routeHandlerContext, next) => async (context) =>
{
var response = await next(context);
Assert.IsType<EmptyHttpResult>(response);
return response;
}
}),
});
var requestDelegate = factoryResult.RequestDelegate;

await requestDelegate(httpContext);
Expand Down Expand Up @@ -6604,6 +6616,34 @@ public void Create_Populates_EndpointBuilderWithRequestDelegateAndMetadata()
Assert.Same(options.EndpointBuilder.Metadata, result.EndpointMetadata);
}

[Fact]
public async Task RDF_CanAssertOnEmptyResult()
{
var @delegate = (string name, HttpContext context) => context.Items.Add("param", name);

var result = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions()
{
EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>()
{
(routeHandlerContext, next) => async (context) =>
{
var response = await next(context);
Assert.IsType<EmptyHttpResult>(response);
Assert.Same(Results.Empty, response);
return response;
}
}),
});

var httpContext = CreateHttpContext();
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
{
["name"] = "Tester"
});

await result.RequestDelegate(httpContext);
}

private DefaultHttpContext CreateHttpContext()
{
var responseFeature = new TestHttpResponseFeature();
Expand Down