Skip to content
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

Get EmptyHttpResult in RDF via reflection #45354

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
35 changes: 11 additions & 24 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ 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 NewExpression EmptyHttpResultValueTaskExpr = Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(IResult) })!, Expression.Property(null, Type.GetType("Microsoft.AspNetCore.Http.Results, Microsoft.AspNetCore.Http.Results")!.GetProperty("Empty")!));
private static readonly object? EmptyHttpResultInstance = Type.GetType("Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult, Microsoft.AspNetCore.Http.Results")!.GetProperty("Instance")!.GetValue(null, null);
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 @@ -2151,32 +2154,34 @@ static async Task ExecuteAwaited(ValueTask task)

private static ValueTask<object?> ExecuteTaskWithEmptyResult(Task task)
{
Debug.Assert(EmptyHttpResultInstance is not null);
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);
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 @@ -2498,24 +2503,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
41 changes: 40 additions & 1 deletion src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing.Patterns;
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 @@ -7050,6 +7062,33 @@ 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);
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