Skip to content

Commit 73a2980

Browse files
committed
Get EmptyHttpResult in RDF via reflection (#45878)
* Get EmptyHttpResult in RDF via reflection * Always use instance property for checks * Favor Debug.Assert instead of early exception * Throw exception in RELEASE builds * Fix ifdef
1 parent 5fa89a8 commit 73a2980

File tree

2 files changed

+59
-25
lines changed

2 files changed

+59
-25
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,17 @@ public static partial class RequestDelegateFactory
9191
private static readonly MemberExpression FormFilesExpr = Expression.Property(FormExpr, typeof(IFormCollection).GetProperty(nameof(IFormCollection.Files))!);
9292
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
9393
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
94-
private static readonly NewExpression EmptyHttpResultValueTaskExpr = Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(EmptyHttpResult) })!, Expression.Property(null, typeof(EmptyHttpResult), nameof(EmptyHttpResult.Instance)));
95-
94+
// Due to https://github.com/dotnet/aspnetcore/issues/41330 we cannot reference the EmptyHttpResult type
95+
// but users still need to assert on it as in https://github.com/dotnet/aspnetcore/issues/45063
96+
// so we temporarily work around this here by using reflection to get the actual type.
97+
private static readonly object? EmptyHttpResultInstance = Type.GetType("Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult, Microsoft.AspNetCore.Http.Results")?.GetProperty("Instance")?.GetValue(null, null);
98+
#if DEBUG
99+
private static readonly NewExpression EmptyHttpResultValueTaskExpr = EmptyHttpResultInstance is not null
100+
? Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(IResult) })!, Expression.Constant(EmptyHttpResultInstance))
101+
: throw new UnreachableException("The EmptyHttpResult type could not be found.");
102+
#else
103+
private static readonly NewExpression EmptyHttpResultValueTaskExpr = Expression.New(typeof(ValueTask<object>).GetConstructor(new[] { typeof(IResult) })!, Expression.Constant(EmptyHttpResultInstance));
104+
#endif
96105
private static readonly ParameterExpression TempSourceStringExpr = ParameterBindingMethodCache.TempSourceStringExpr;
97106
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
98107
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
@@ -389,6 +398,7 @@ private static Expression[] CreateArgumentsAndInferMetadata(MethodInfo methodInf
389398
private static EndpointFilterDelegate? CreateFilterPipeline(MethodInfo methodInfo, Expression? targetExpression, RequestDelegateFactoryContext factoryContext, Expression<Func<HttpContext, object?>>? targetFactory)
390399
{
391400
Debug.Assert(factoryContext.EndpointBuilder.FilterFactories.Count > 0);
401+
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
392402
// httpContext.Response.StatusCode >= 400
393403
// ? Task.CompletedTask
394404
// : {
@@ -453,6 +463,7 @@ targetExpression is null
453463

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

20982109
private static ValueTask<object?> ExecuteTaskWithEmptyResult(Task task)
20992110
{
2111+
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
21002112
static async ValueTask<object?> ExecuteAwaited(Task task)
21012113
{
21022114
await task;
2103-
return EmptyHttpResult.Instance;
2115+
return EmptyHttpResultInstance;
21042116
}
21052117

21062118
if (task.IsCompletedSuccessfully)
21072119
{
2108-
return new ValueTask<object?>(EmptyHttpResult.Instance);
2120+
return new ValueTask<object?>(EmptyHttpResultInstance);
21092121
}
21102122

21112123
return ExecuteAwaited(task);
21122124
}
21132125

21142126
private static ValueTask<object?> ExecuteValueTaskWithEmptyResult(ValueTask valueTask)
21152127
{
2128+
Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found.");
21162129
static async ValueTask<object?> ExecuteAwaited(ValueTask task)
21172130
{
21182131
await task;
2119-
return EmptyHttpResult.Instance;
2132+
return EmptyHttpResultInstance;
21202133
}
21212134

21222135
if (valueTask.IsCompletedSuccessfully)
21232136
{
21242137
valueTask.GetAwaiter().GetResult();
2125-
return new ValueTask<object?>(EmptyHttpResult.Instance);
2138+
return new ValueTask<object?>(EmptyHttpResultInstance);
21262139
}
21272140

21282141
return ExecuteAwaited(valueTask);
@@ -2442,24 +2455,6 @@ private static void FormatTrackedParameters(RequestDelegateFactoryContext factor
24422455
}
24432456
}
24442457

2445-
// Due to cyclic references between Http.Extensions and
2446-
// Http.Results, we define our own instance of the `EmptyHttpResult`
2447-
// type here.
2448-
private sealed class EmptyHttpResult : IResult
2449-
{
2450-
private EmptyHttpResult()
2451-
{
2452-
}
2453-
2454-
public static EmptyHttpResult Instance { get; } = new();
2455-
2456-
/// <inheritdoc/>
2457-
public Task ExecuteAsync(HttpContext httpContext)
2458-
{
2459-
return Task.CompletedTask;
2460-
}
2461-
}
2462-
24632458
private sealed class RDFEndpointBuilder : EndpointBuilder
24642459
{
24652460
public RDFEndpointBuilder(IServiceProvider applicationServices)

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,18 @@ public async Task RequestDelegateInvokesAction(Delegate @delegate)
9797
{
9898
var httpContext = CreateHttpContext();
9999

100-
var factoryResult = RequestDelegateFactory.Create(@delegate);
100+
var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions()
101+
{
102+
EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>()
103+
{
104+
(routeHandlerContext, next) => async (context) =>
105+
{
106+
var response = await next(context);
107+
Assert.IsType<EmptyHttpResult>(response);
108+
return response;
109+
}
110+
}),
111+
});
101112
var requestDelegate = factoryResult.RequestDelegate;
102113

103114
await requestDelegate(httpContext);
@@ -6604,6 +6615,34 @@ public void Create_Populates_EndpointBuilderWithRequestDelegateAndMetadata()
66046615
Assert.Same(options.EndpointBuilder.Metadata, result.EndpointMetadata);
66056616
}
66066617

6618+
[Fact]
6619+
public async Task RDF_CanAssertOnEmptyResult()
6620+
{
6621+
var @delegate = (string name, HttpContext context) => context.Items.Add("param", name);
6622+
6623+
var result = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions()
6624+
{
6625+
EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List<Func<EndpointFilterFactoryContext, EndpointFilterDelegate, EndpointFilterDelegate>>()
6626+
{
6627+
(routeHandlerContext, next) => async (context) =>
6628+
{
6629+
var response = await next(context);
6630+
Assert.IsType<EmptyHttpResult>(response);
6631+
Assert.Same(Results.Empty, response);
6632+
return response;
6633+
}
6634+
}),
6635+
});
6636+
6637+
var httpContext = CreateHttpContext();
6638+
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
6639+
{
6640+
["name"] = "Tester"
6641+
});
6642+
6643+
await result.RequestDelegate(httpContext);
6644+
}
6645+
66076646
private DefaultHttpContext CreateHttpContext()
66086647
{
66096648
var responseFeature = new TestHttpResponseFeature();

0 commit comments

Comments
 (0)