diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 079d432979b0..d652e5156f16 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -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>(() => Task.CompletedTask)); - private static readonly NewExpression EmptyHttpResultValueTaskExpr = Expression.New(typeof(ValueTask).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).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)); @@ -2154,15 +2157,16 @@ static async Task ExecuteAwaited(ValueTask task) private static ValueTask ExecuteTaskWithEmptyResult(Task task) { + Debug.Assert(EmptyHttpResultInstance is not null); static async ValueTask ExecuteAwaited(Task task) { await task; - return EmptyHttpResult.Instance; + return EmptyHttpResultInstance; } if (task.IsCompletedSuccessfully) { - return new ValueTask(EmptyHttpResult.Instance); + return new ValueTask(EmptyHttpResultInstance); } return ExecuteAwaited(task); @@ -2170,16 +2174,17 @@ static async Task ExecuteAwaited(ValueTask task) private static ValueTask ExecuteValueTaskWithEmptyResult(ValueTask valueTask) { + Debug.Assert(EmptyHttpResultInstance is not null); static async ValueTask ExecuteAwaited(ValueTask task) { await task; - return EmptyHttpResult.Instance; + return EmptyHttpResultInstance; } if (valueTask.IsCompletedSuccessfully) { valueTask.GetAwaiter().GetResult(); - return new ValueTask(EmptyHttpResult.Instance); + return new ValueTask(EmptyHttpResultInstance); } return ExecuteAwaited(valueTask); @@ -2507,24 +2512,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(); - - /// - public Task ExecuteAsync(HttpContext httpContext) - { - return Task.CompletedTask; - } - } - private sealed class RdfEndpointBuilder : EndpointBuilder { public RdfEndpointBuilder(IServiceProvider applicationServices) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index af41f16d6ab2..f174015efc69 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -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; @@ -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>() + { + (routeHandlerContext, next) => async (context) => + { + var response = await next(context); + Assert.IsType(response); + return response; + } + }), + }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -7075,6 +7087,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>() + { + (routeHandlerContext, next) => async (context) => + { + var response = await next(context); + Assert.IsType(response); + Assert.Same(Results.Empty, response); + return response; + } + }), + }); + + var httpContext = CreateHttpContext(); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "Tester" + }); + + await result.RequestDelegate(httpContext); + } + private class ParameterListRequiredStringFromDifferentSources { public HttpContext? HttpContext { get; set; }