From 65330ff0e2e88e7c4aa8751c1caceaf4da7a7b13 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 12 Jan 2023 11:37:02 -0800 Subject: [PATCH] Get EmptyHttpResult in RDF via reflection (#45878) (#46048) * 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 * Add HttpResults using --- .../src/RequestDelegateFactory.cs | 43 ++++++++----------- .../test/RequestDelegateFactoryTests.cs | 42 +++++++++++++++++- 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index bef35cdb00d8..2de0a8c858e5 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -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>(() => 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 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).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).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)); @@ -389,6 +398,7 @@ private static Expression[] CreateArgumentsAndInferMetadata(MethodInfo methodInf private static EndpointFilterDelegate? CreateFilterPipeline(MethodInfo methodInfo, Expression? targetExpression, RequestDelegateFactoryContext factoryContext, Expression>? 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 // : { @@ -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); @@ -2097,15 +2108,16 @@ static async Task ExecuteAwaited(ValueTask task) private static ValueTask ExecuteTaskWithEmptyResult(Task task) { + Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found."); 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); @@ -2113,16 +2125,17 @@ static async Task ExecuteAwaited(ValueTask task) private static ValueTask ExecuteValueTaskWithEmptyResult(ValueTask valueTask) { + Debug.Assert(EmptyHttpResultInstance is not null, "The EmptyHttpResult type could not be found."); 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); @@ -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(); - - /// - 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 a4b30026e6b6..f8596b212ec3 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -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; @@ -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); @@ -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>() + { + (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 DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature();