From d818c5b50522d3be828993305f3f41e25bfc1205 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 15:36:10 -0500 Subject: [PATCH 01/20] Remove unneeded unboxing/boxing --- .../ObjectMethodExecutorFSharpSupport.cs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index 95970685f6cb..36501df8ebfc 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -51,25 +51,27 @@ public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single(); awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType); - // coerceToAwaitableExpression = (object fsharpAsync) => - // { - // return (object)FSharpAsync.StartAsTask( - // (Microsoft.FSharp.Control.FSharpAsync)fsharpAsync, - // FSharpOption.None, - // FSharpOption.None); - // }; + // We simply want to call `FSharpAsync.StartAsTask` on the F# async value: + // + // coerceToAwaitableExpression = (FSharpAsync fsharpAsync) => + // { + // return FSharpAsync.StartAsTask( + // fsharpAsync, + // FSharpOption.None, + // FSharpOption.None); + // }; + // var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod .MakeGenericMethod(awaiterResultType); - var coerceToAwaitableParam = Expression.Parameter(typeof(object)); - coerceToAwaitableExpression = Expression.Lambda( - Expression.Convert( - Expression.Call( - startAsTaskClosedMethod, - Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType), - Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), - Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), - typeof(object)), - coerceToAwaitableParam); + var coerceToAwaitableParam = Expression.Parameter(possibleFSharpAsyncType); + coerceToAwaitableExpression = + Expression.Lambda( + body: Expression.Call( + method: startAsTaskClosedMethod, + arg0: Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType), + arg1: Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), + arg2: Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), + parameters: coerceToAwaitableParam); return true; } From b36b82a782e43e1a4828591f247a05a93181912a Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 15:36:40 -0500 Subject: [PATCH 02/20] Add doc comment --- .../ObjectMethodExecutorFSharpSupport.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index 36501df8ebfc..162ee92e50d4 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -31,6 +31,26 @@ internal static class ObjectMethodExecutorFSharpSupport private static PropertyInfo _fsharpOptionOfTaskCreationOptionsNoneProperty; private static PropertyInfo _fsharpOptionOfCancellationTokenNoneProperty; + /// + /// Builds a for converting a value of the given generic instantiation of + /// FSharp.Control.FSharpAsync<T> + /// to a , if is in fact a closed F# async type. + /// + /// + /// The type that is a potential generic instantiation of + /// FSharp.Control.FSharpAsync<T>. + /// + /// + /// When this method returns, contains a for converting a value of type + /// to a , if is a generic instantiation of + /// FSharp.Control.FSharpAsync<T>, + /// or if it is not. + /// + /// + /// When this method returns, contains the type of the closed generic instantiation of that will be returned + /// by the coercer expression, if it was possible to build a coercer, or if not. + /// + /// if it was possible to build a coercer; otherwise, . [UnconditionalSuppressMessage("Trimmer", "IL2060", Justification = "Reflecting over the async FSharpAsync<> contract.")] public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( Type possibleFSharpAsyncType, From 83877465f5f037998097112fc43243cc141de380 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 15:36:58 -0500 Subject: [PATCH 03/20] Remove unused using --- .../ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index 162ee92e50d4..f6e6f5ff591f 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -3,13 +3,10 @@ #nullable disable -using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Extensions.Internal; From f2c23be566cd34ef33c78c418e585e0066041e76 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 15:39:30 -0500 Subject: [PATCH 04/20] Convert F# Async<> to Task<> in request delegates * Use `CoercedAwaitableInfo` to convert `FSharp.Control.FSharpAsync` values to `System.Threading.Tasks.Task` when building request delegates and populate endpoint metadata accordingly. --- .../src/RequestDelegateFactory.cs | 84 +++++++++---------- src/Shared/EndpointMetadataPopulator.cs | 4 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 9afda2e748bc..055a841a36b6 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -487,34 +487,41 @@ private static Expression MapHandlerReturnTypeToValueTask(Expression methodCall, { return Expression.Block(methodCall, EmptyHttpResultValueTaskExpr); } - else if (returnType == typeof(Task)) + else if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo)) { - return Expression.Call(ExecuteTaskWithEmptyResultMethod, methodCall); - } - else if (returnType == typeof(ValueTask)) - { - return Expression.Call(ExecuteValueTaskWithEmptyResultMethod, methodCall); - } - else if (returnType == typeof(ValueTask)) - { - return methodCall; - } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - var typeArg = returnType.GetGenericArguments()[0]; - return Expression.Call(ValueTaskOfTToValueTaskOfObjectMethod.MakeGenericMethod(typeArg), methodCall); - } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - var typeArg = returnType.GetGenericArguments()[0]; - return Expression.Call(TaskOfTToValueTaskOfObjectMethod.MakeGenericMethod(typeArg), methodCall); - } - else - { - return Expression.Call(WrapObjectAsValueTaskMethod, methodCall); + if (coercedAwaitableInfo.CoercerExpression is LambdaExpression coercerExpression) + { + returnType = coercerExpression.ReturnType; + methodCall = Expression.Invoke(coercerExpression, methodCall); + } + + if (returnType == typeof(Task)) + { + return Expression.Call(ExecuteTaskWithEmptyResultMethod, methodCall); + } + else if (returnType == typeof(ValueTask)) + { + return Expression.Call(ExecuteValueTaskWithEmptyResultMethod, methodCall); + } + else if (returnType == typeof(ValueTask)) + { + return methodCall; + } + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var typeArg = coercedAwaitableInfo.AwaitableInfo.ResultType; + return Expression.Call(ValueTaskOfTToValueTaskOfObjectMethod.MakeGenericMethod(typeArg), methodCall); + } + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + var typeArg = coercedAwaitableInfo.AwaitableInfo.ResultType; + return Expression.Call(TaskOfTToValueTaskOfObjectMethod.MakeGenericMethod(typeArg), methodCall); + } } + + return Expression.Call(WrapObjectAsValueTaskMethod, methodCall); } private static ValueTask ValueTaskOfTToValueTaskOfObject(ValueTask valueTask) @@ -953,22 +960,9 @@ private static void PopulateBuiltInResponseTypeMetadata(Type returnType, Endpoin throw GetUnsupportedReturnTypeException(returnType); } - if (returnType == typeof(Task) || returnType == typeof(ValueTask)) + if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo)) { - returnType = typeof(void); - } - else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) - { - var genericTypeDefinition = returnType.IsGenericType ? returnType.GetGenericTypeDefinition() : null; - - if (genericTypeDefinition == typeof(Task<>) || genericTypeDefinition == typeof(ValueTask<>)) - { - returnType = returnType.GetGenericArguments()[0]; - } - else - { - throw GetUnsupportedReturnTypeException(returnType); - } + returnType = coercedAwaitableInfo.AwaitableInfo.ResultType; } // Skip void returns and IResults. IResults might implement IEndpointMetadataProvider but otherwise we don't know what it might do. @@ -1019,8 +1013,14 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, factoryContext.JsonSerializerOptionsExpression, Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo))); } - else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) + else if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo)) { + if (coercedAwaitableInfo.CoercerExpression is LambdaExpression coercerExpression) + { + returnType = coercerExpression.ReturnType; + methodCall = Expression.Invoke(coercerExpression, methodCall); + } + if (returnType == typeof(Task)) { return methodCall; diff --git a/src/Shared/EndpointMetadataPopulator.cs b/src/Shared/EndpointMetadataPopulator.cs index c17dbf818d59..2c8fe12d3e9e 100644 --- a/src/Shared/EndpointMetadataPopulator.cs +++ b/src/Shared/EndpointMetadataPopulator.cs @@ -47,9 +47,9 @@ public static void PopulateMetadata(MethodInfo methodInfo, EndpointBuilder build // Get metadata from return type var returnType = methodInfo.ReturnType; - if (AwaitableInfo.IsTypeAwaitable(returnType, out var awaitableInfo)) + if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo)) { - returnType = awaitableInfo.ResultType; + returnType = coercedAwaitableInfo.AwaitableInfo.ResultType; } if (returnType is not null && typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType)) From 205c312fa95998ebb0f880b78286d223f280c996 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 16:11:52 -0500 Subject: [PATCH 05/20] Add tests for F# async request delegate support --- ...ft.AspNetCore.Http.Extensions.Tests.csproj | 1 + .../test/RequestDelegateFactoryTests.cs | 140 +++++++++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index b6a0988e5a9f..7c1c9daa8ecc 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 302fc28437da..dcaafe61c216 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -2964,19 +2964,23 @@ public static IEnumerable ComplexResult Todo TestAction() => originalTodo; Task TaskTestAction() => Task.FromResult(originalTodo); ValueTask ValueTaskTestAction() => ValueTask.FromResult(originalTodo); + FSharp.Control.FSharpAsync FSharpAsyncTestAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(originalTodo); static Todo StaticTestAction() => new Todo { Name = "Write even more tests!" }; static Task StaticTaskTestAction() => Task.FromResult(new Todo { Name = "Write even more tests!" }); static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new Todo { Name = "Write even more tests!" }); + static FSharp.Control.FSharpAsync StaticFSharpAsyncTestAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new Todo { Name = "Write even more tests!" }); return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction}, new object[] { (Func>)ValueTaskTestAction}, + new object[] { (Func>)FSharpAsyncTestAction}, new object[] { (Func)StaticTestAction}, new object[] { (Func>)StaticTaskTestAction}, new object[] { (Func>)StaticValueTaskTestAction}, + new object[] { (Func>)StaticFSharpAsyncTestAction}, }; } } @@ -6437,6 +6441,74 @@ public async Task CanInvokeFilter_OnValueTaskOfTReturningHandler(Delegate @deleg Assert.Equal("foo", decodedResponseBody); } + public static object[][] FSharpAsyncOfTMethods + { + get + { + FSharp.Control.FSharpAsync FSharpAsyncOfTMethod() + { + return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return("foo"); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfTWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return "foo"; + } + } + + FSharp.Control.FSharpAsync FSharpAsyncOfObjectWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return "foo"; + } + } + + return new object[][] + { + new object[] { (Func>)FSharpAsyncOfTMethod }, + new object[] { (Func>)FSharpAsyncOfTWithYieldMethod }, + new object[] { (Func>)FSharpAsyncOfObjectWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(FSharpAsyncOfTMethods))] + public async Task CanInvokeFilter_OnFSharpAsyncOfTReturningHandler(Delegate @delegate) + { + // Arrange + var responseBodyStream = new MemoryStream(); + var httpContext = CreateHttpContext(); + httpContext.Response.Body = responseBodyStream; + + // Act + var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + { + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() + { + (routeHandlerContext, next) => async (context) => + { + return await next(context); + } + }), + }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal("foo", decodedResponseBody); + } + public static object[][] VoidReturningMethods { get @@ -6565,12 +6637,30 @@ async Task TaskOfStructWithYieldMethod() return new TodoStruct { Name = "Test todo" }; } + FSharp.Control.FSharpAsync FSharpAsyncOfStructMethod() + { + return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new TodoStruct { Name = "Test todo" }); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfStructWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return new TodoStruct { Name = "Test todo" }; + } + } + return new object[][] { new object[] { (Func>)ValueTaskOfStructMethod }, new object[] { (Func>)ValueTaskOfStructWithYieldMethod }, new object[] { (Func>)TaskOfStructMethod }, - new object[] { (Func>)TaskOfStructWithYieldMethod } + new object[] { (Func>)TaskOfStructWithYieldMethod }, + new object[] { (Func>)FSharpAsyncOfStructMethod }, + new object[] { (Func>)FSharpAsyncOfStructWithYieldMethod } }; } } @@ -6806,6 +6896,19 @@ public void Create_DiscoversEndpointMetadata_FromValueTaskWrappedReturnTypeImple Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); } + [Fact] + public void Create_DiscoversEndpointMetadata_FromFSharpAsyncWrappedReturnTypeImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = () => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new AddsCustomEndpointMetadataResult()); + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.ReturnType }); + } + [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromReturnTypesImplementingIEndpointMetadataProvider() { @@ -6872,6 +6975,28 @@ public void Create_CombinesDefaultMetadata_AndMetadataFromValueTaskWrappedReturn Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); } + [Fact] + public void Create_CombinesDefaultMetadata_AndMetadataFromFSharpAsyncWrappedReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = () => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CountsDefaultEndpointMetadataResult()); + var options = new RequestDelegateFactoryOptions + { + EndpointBuilder = CreateEndpointBuilder(new List + { + new CustomEndpointMetadata { Source = MetadataSource.Caller } + }), + }; + + // Act + var result = RequestDelegateFactory.Create(@delegate, options); + + // Assert + Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Caller }); + // Expecting '1' because only initial metadata will be in the metadata list when this metadata item is added + Assert.Contains(result.EndpointMetadata, m => m is MetadataCountMetadata { Count: 1 }); + } + [Fact] public void Create_CombinesDefaultMetadata_AndMetadataFromParameterTypesImplementingIEndpointParameterMetadataProvider() { @@ -7138,6 +7263,19 @@ public void Create_AllowsRemovalOfDefaultMetadata_ByValueTaskWrappedReturnTypesI Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); } + [Fact] + public void Create_AllowsRemovalOfDefaultMetadata_ByFSharpAsyncWrappedReturnTypesImplementingIEndpointMetadataProvider() + { + // Arrange + var @delegate = (Todo todo) => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new RemovesAcceptsMetadataResult()); + + // Act + var result = RequestDelegateFactory.Create(@delegate); + + // Assert + Assert.DoesNotContain(result.EndpointMetadata, m => m is IAcceptsMetadata); + } + [Fact] public void Create_AllowsRemovalOfDefaultMetadata_ByParameterTypesImplementingIEndpointParameterMetadataProvider() { From 3b43ac23b53921a4cec13807380221e12aa15b27 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 16:14:12 -0500 Subject: [PATCH 06/20] F# async not supported by source generator * Add a comment to the request delegate source generator tests that explicitly calls out that F# async is not currently supported by the source generator. --- .../RequestDelegateGeneratorTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 8766a3f5e58e..6ea15062d166 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -387,7 +387,11 @@ public async Task MapAction_NoParam_ValueTaskOfTReturn(string source, string exp new object[] { @"app.MapGet(""/"", () => new ValueTask(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""" }, new object[] { @"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""" }, new object[] { @"app.MapGet(""/"", () => new ValueTask(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""" }, - new object[] { @"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""" } + new object[] { @"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""" }, + // Not currently supported by the source generator: + // new object[] { @"app.MapGet(""/"", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(""Hello world!""));", "Hello world!" }, + // new object[] { @"app.MapGet(""/"", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""" }, + // new object[] { @"app.MapGet(""/"", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""" } }; [Theory] From 5a30f8614b60239ea3bbfdc61e6097d8dab1c160 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 19:11:06 -0500 Subject: [PATCH 07/20] Actually remove extra convert call --- .../ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index f6e6f5ff591f..f8cdfc12fbae 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -85,7 +85,7 @@ public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( Expression.Lambda( body: Expression.Call( method: startAsTaskClosedMethod, - arg0: Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType), + arg0: coerceToAwaitableParam, arg1: Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), arg2: Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), parameters: coerceToAwaitableParam); From 2b42bae70bc68f0499285840bbbb8d372b20731f Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Feb 2023 19:11:44 -0500 Subject: [PATCH 08/20] Shrink diff --- .../ObjectMethodExecutorFSharpSupport.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index f8cdfc12fbae..59b0072461dd 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -68,16 +68,13 @@ public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single(); awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType); - // We simply want to call `FSharpAsync.StartAsTask` on the F# async value: - // - // coerceToAwaitableExpression = (FSharpAsync fsharpAsync) => - // { - // return FSharpAsync.StartAsTask( - // fsharpAsync, - // FSharpOption.None, - // FSharpOption.None); - // }; - // + // coerceToAwaitableExpression = (FSharpAsync fsharpAsync) => + // { + // return FSharpAsync.StartAsTask( + // fsharpAsync, + // FSharpOption.None, + // FSharpOption.None); + // }; var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod .MakeGenericMethod(awaiterResultType); var coerceToAwaitableParam = Expression.Parameter(possibleFSharpAsyncType); From 17bf70a80af771528d339e1993b4ab98b44eeea4 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 19 Mar 2023 15:32:03 -0400 Subject: [PATCH 09/20] Use CoercedAwaitableInfo in OpenApi gen --- src/OpenApi/src/OpenApiGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs index 9a874fd2663d..f83b52b48673 100644 --- a/src/OpenApi/src/OpenApiGenerator.cs +++ b/src/OpenApi/src/OpenApiGenerator.cs @@ -98,9 +98,9 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM { var responses = new OpenApiResponses(); var responseType = method.ReturnType; - if (AwaitableInfo.IsTypeAwaitable(responseType, out var awaitableInfo)) + if (CoercedAwaitableInfo.IsTypeAwaitable(responseType, out var coercedAwaitableInfo)) { - responseType = awaitableInfo.ResultType; + responseType = coercedAwaitableInfo.AwaitableInfo.ResultType; } if (typeof(IResult).IsAssignableFrom(responseType)) From 25cb783b118548bf96c96d563dce3455404b05b5 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 19 Mar 2023 15:32:50 -0400 Subject: [PATCH 10/20] Use CoercedAwaitableInfo in EndpointMetadataApiDescriptionProvider --- .../src/EndpointMetadataApiDescriptionProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 8695097020fd..8acb224a26ca 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -326,9 +326,9 @@ private static void AddSupportedResponseTypes( { var responseType = returnType; - if (AwaitableInfo.IsTypeAwaitable(responseType, out var awaitableInfo)) + if (CoercedAwaitableInfo.IsTypeAwaitable(responseType, out var coercedAwaitableInfo)) { - responseType = awaitableInfo.ResultType; + responseType = coercedAwaitableInfo.AwaitableInfo.ResultType; } // Can't determine anything about IResults yet that's not from extra metadata. IResult could help here. From 5242ba98ec8bab1dc0181a6c605d7b85a1b6f56d Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 19 Mar 2023 15:33:51 -0400 Subject: [PATCH 11/20] Add FSharpAsync tests for EndpointMetadataApiDescriptionProvider --- .../EndpointMetadataProviderTest.cs | 16 ++++++++++++++++ .../Microsoft.AspNetCore.Mvc.Core.Test.csproj | 1 + 2 files changed, 17 insertions(+) diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs index 84e43647a399..98f5d8e0253c 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs @@ -25,6 +25,8 @@ public class EndpointMetadataProviderTest [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInValueTaskOfActionResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInTaskOfResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInTaskOfActionResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInFSharpAsyncOfResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInFSharpAsyncOfActionResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithMetadataInActionResult))] public void DiscoversEndpointMetadata_FromReturnTypeImplementingIEndpointMetadataProvider(Type controllerType, string actionName) @@ -131,6 +133,8 @@ public void DiscoversMetadata_CorrectOrder() [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInValueTaskOfActionResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInTaskOfResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInTaskOfActionResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInFSharpAsyncOfResult))] + [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInFSharpAsyncOfActionResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInResult))] [InlineData(typeof(TestController), nameof(TestController.ActionWithNoAcceptsMetadataInActionResult))] public void AllowsRemovalOfMetadata_ByReturnTypeImplementingIEndpointMetadataProvider(Type controllerType, string actionName) @@ -314,6 +318,9 @@ public ValueTask ActionWithMetadataInValueTask public Task ActionWithMetadataInTaskOfResult() => Task.FromResult(null); + public FSharp.Control.FSharpAsync ActionWithMetadataInFSharpAsyncOfResult() + => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(null); + [HttpGet("selector1")] [HttpGet("selector2")] public AddsCustomEndpointMetadataActionResult MultipleSelectorsActionWithMetadataInActionResult() => null; @@ -326,6 +333,9 @@ public ValueTask ActionWithMetadataInVal public Task ActionWithMetadataInTaskOfActionResult() => Task.FromResult(null); + public FSharp.Control.FSharpAsync ActionWithMetadataInFSharpAsyncOfActionResult() + => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(null); + public RemovesAcceptsMetadataResult ActionWithNoAcceptsMetadataInResult() => null; public ValueTask ActionWithNoAcceptsMetadataInValueTaskOfResult() @@ -334,6 +344,9 @@ public ValueTask ActionWithNoAcceptsMetadataInValu public Task ActionWithNoAcceptsMetadataInTaskOfResult() => Task.FromResult(null); + public FSharp.Control.FSharpAsync ActionWithNoAcceptsMetadataInFSharpAsyncOfResult() + => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(null); + public RemovesAcceptsMetadataActionResult ActionWithNoAcceptsMetadataInActionResult() => null; public ValueTask ActionWithNoAcceptsMetadataInValueTaskOfActionResult() @@ -341,6 +354,9 @@ public ValueTask ActionWithNoAcceptsMetadata public Task ActionWithNoAcceptsMetadataInTaskOfActionResult() => Task.FromResult(null); + + public FSharp.Control.FSharpAsync ActionWithNoAcceptsMetadataInFSharpAsyncOfActionResult() + => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(null); } private class CustomEndpointMetadata diff --git a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj index 4fa47849805e..e7ccf9267d05 100644 --- a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj +++ b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj @@ -10,6 +10,7 @@ + From 7a0ac173fccfb7357916ce861f81d07a6b480868 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 19 Mar 2023 15:34:42 -0400 Subject: [PATCH 12/20] Add runtime creation test for FSharpAsync --- .../RuntimeCreationTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs index b3338c85745c..d2cc624aebab 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs @@ -21,4 +21,26 @@ public async Task MapAction_BindAsync_WithWrongType_IsNotUsed(string bindAsyncTy var ex = Assert.Throws(() => GetEndpointFromCompilation(compilation)); Assert.StartsWith($"BindAsync method found on {bindAsyncType} with incorrect format.", ex.Message); } + + [Theory] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return("Hello"));""", "Hello")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new Todo { Name = "Hello" }));""", """{"id":0,"name":"Hello","isComplete":false}""")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(TypedResults.Ok(new Todo { Name = "Hello" })));""", """{"id":0,"name":"Hello","isComplete":false}""")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(Microsoft.FSharp.Core.Unit)));""", "null")] + public async Task MapAction_RuntimeCreation_FSharpAsync_IsAwaitable(string source, string expectedBody) + { + var (result, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(result, endpointModel => + { + Assert.Equal("/", endpointModel.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + }); + + var httpContext = CreateHttpContext(); + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, expectedBody); + } } From be473f1482e910f88a5e30095154b736a630d9b6 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Mar 2023 13:27:07 -0400 Subject: [PATCH 13/20] Convert unit awaitables to void awaitables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * FSharpAsync → Task * Task → Task * ValueTask → ValueTask --- .../src/RequestDelegateFactory.cs | 8 +- .../CoercedAwaitableInfo.cs | 40 +++-- .../ObjectMethodExecutorFSharpSupport.cs | 147 ++++++++++++++---- 3 files changed, 150 insertions(+), 45 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index e55b0f4e868b..c7f57b1dba35 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -489,9 +489,13 @@ private static Expression MapHandlerReturnTypeToValueTask(Expression methodCall, } else if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo)) { - if (coercedAwaitableInfo.CoercerExpression is LambdaExpression coercerExpression) + if (coercedAwaitableInfo.CoercerResultType is { } coercedType) + { + returnType = coercedType; + } + + if (coercedAwaitableInfo.CoercerExpression is { } coercerExpression) { - returnType = coercerExpression.ReturnType; methodCall = Expression.Invoke(coercerExpression, methodCall); } diff --git a/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs b/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs index 4951c218b9a8..68d763977bdb 100644 --- a/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs +++ b/src/Shared/ObjectMethodExecutor/CoercedAwaitableInfo.cs @@ -36,24 +36,38 @@ public static bool IsTypeAwaitable( { if (AwaitableInfo.IsTypeAwaitable(type, out var directlyAwaitableInfo)) { - info = new CoercedAwaitableInfo(directlyAwaitableInfo); + // Convert {Value}Task to non-generic {Value}Task. + if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromUnitAwaitableToVoidAwaitable(type, + out var coercerExpression, + out var nonGenericAwaitableType)) + { + _ = AwaitableInfo.IsTypeAwaitable(nonGenericAwaitableType, out directlyAwaitableInfo); + info = new CoercedAwaitableInfo(coercerExpression, nonGenericAwaitableType, directlyAwaitableInfo); + } + else + { + info = new CoercedAwaitableInfo(directlyAwaitableInfo); + } + return true; } - - // It's not directly awaitable, but maybe we can coerce it. - // Currently we support coercing FSharpAsync. - if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type, - out var coercerExpression, - out var coercerResultType)) + else { - if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo)) + // It's not directly awaitable, but maybe we can coerce it. + // Currently we support coercing FSharpAsync. + if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type, + out var coercerExpression, + out var coercerResultType)) { - info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo); - return true; + if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo)) + { + info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo); + return true; + } } - } - info = default(CoercedAwaitableInfo); - return false; + info = default(CoercedAwaitableInfo); + return false; + } } } diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index 59b0072461dd..f270977006b0 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -11,8 +11,7 @@ namespace Microsoft.Extensions.Internal; /// -/// Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying -/// an for mapping instances of that type to a C# awaitable. +/// Helper for converting F# async values to their BCL analogues. /// /// /// The main design goal here is to avoid taking a compile-time dependency on @@ -22,6 +21,11 @@ namespace Microsoft.Extensions.Internal; [RequiresDynamicCode("Dynamically generates calls to FSharpAsync.")] internal static class ObjectMethodExecutorFSharpSupport { + private const string FSharpAsyncGenericTypeName = "Microsoft.FSharp.Control.FSharpAsync`1"; + private const string FSharpAsyncTypeName = "Microsoft.FSharp.Control.FSharpAsync"; + private const string FSharpUnitTypeName = "Microsoft.FSharp.Core.Unit"; + private const string FSharpOptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1"; + private static readonly object _fsharpValuesCacheLock = new object(); private static Assembly _fsharpCoreAssembly; private static MethodInfo _fsharpAsyncStartAsTaskGenericMethod; @@ -31,21 +35,23 @@ internal static class ObjectMethodExecutorFSharpSupport /// /// Builds a for converting a value of the given generic instantiation of /// FSharp.Control.FSharpAsync<T> - /// to a , if is in fact a closed F# async type. + /// to a , if is in fact a closed F# async type, + /// or to a , if TResult is FSharp.Core.Unit. /// /// /// The type that is a potential generic instantiation of /// FSharp.Control.FSharpAsync<T>. /// /// - /// When this method returns, contains a for converting a value of type - /// to a , if is a generic instantiation of + /// When this method returns and is a generic instantiation of /// FSharp.Control.FSharpAsync<T>, - /// or if it is not. + /// contains a for converting a value of type + /// to a , or to a , if TResult is FSharp.Core.Unit; + /// otherwise, . /// /// - /// When this method returns, contains the type of the closed generic instantiation of that will be returned - /// by the coercer expression, if it was possible to build a coercer, or if not. + /// When this method returns, contains the type of the closed generic instantiation of or of that will be returned + /// by the coercer expression, if it was possible to build a coercer; otherwise, . /// /// if it was possible to build a coercer; otherwise, . [UnconditionalSuppressMessage("Trimmer", "IL2060", Justification = "Reflecting over the async FSharpAsync<> contract.")] @@ -75,25 +81,103 @@ public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( // FSharpOption.None, // FSharpOption.None); // }; + var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod .MakeGenericMethod(awaiterResultType); + var coerceToAwaitableParam = Expression.Parameter(possibleFSharpAsyncType); - coerceToAwaitableExpression = - Expression.Lambda( - body: Expression.Call( - method: startAsTaskClosedMethod, - arg0: coerceToAwaitableParam, - arg1: Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), - arg2: Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), - parameters: coerceToAwaitableParam); + + var startAsTaskCall = + Expression.Call( + method: startAsTaskClosedMethod, + arg0: coerceToAwaitableParam, + arg1: Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), + arg2: Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)); + + Expression body = + TryBuildCoercerFromUnitAwaitableToVoidAwaitable(awaitableType, out var coercerExpression, out var nonGenericAwaitableType) + ? Expression.Invoke(coercerExpression, startAsTaskCall) + : startAsTaskCall; + + coerceToAwaitableExpression = Expression.Lambda(body, coerceToAwaitableParam); + + if (nonGenericAwaitableType is not null) + { + awaitableType = nonGenericAwaitableType; + } return true; } - private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType) + /// + /// Builds a for converting a Task<unit> or ValueTask<unit> + /// to a void-returning or . + /// + /// The generic awaitable type to convert. + /// + /// When this method returns and the was + /// Task<unit> or ValueTask<unit>, + /// contains a for converting to the corresponding void-returning awaitable type; + /// otherwise, . + /// + /// + /// When this method returns and the was + /// Task<unit> or ValueTask<unit>, + /// contains the corresponding void-returning awaitable type; + /// otherwise, . + /// + /// if it was possible to build a coercer; otherwise, . + [UnconditionalSuppressMessage("Trimmer", "IL2060", Justification = "Reflecting over FSharp.Core.Unit.")] + public static bool TryBuildCoercerFromUnitAwaitableToVoidAwaitable( + Type genericAwaitableType, + out Expression coercerExpression, + out Type nonGenericAwaitableType) + { + if (!genericAwaitableType.IsGenericType) + { + coercerExpression = null; + nonGenericAwaitableType = null; + return false; + } + + (nonGenericAwaitableType, coercerExpression) = genericAwaitableType.GetGenericTypeDefinition() switch + { + var typeDef when typeDef == typeof(Task<>) && IsFSharpUnit(genericAwaitableType.GetGenericArguments()[0]) => (typeof(Task), MakeTaskOfUnitToTaskExpression(genericAwaitableType)), + var typeDef when typeDef == typeof(ValueTask<>) && IsFSharpUnit(genericAwaitableType.GetGenericArguments()[0]) => (typeof(ValueTask), MakeValueTaskOfUnitToValueTaskExpression(genericAwaitableType)), + _ => default + }; + + return (nonGenericAwaitableType, coercerExpression) is not (null, null); + + static Expression MakeTaskOfUnitToTaskExpression(Type type) + { + var closedGenericTaskParam = Expression.Parameter(type); + return Expression.Lambda(Expression.Convert(closedGenericTaskParam, typeof(Task)), closedGenericTaskParam); + } + + static Expression MakeValueTaskOfUnitToValueTaskExpression(Type type) + { + var closedGenericTaskParam = Expression.Parameter(type); + + var conversionMethod = + typeof(ObjectMethodExecutorFSharpSupport) + .GetMethod(nameof(ConvertValueTaskOfTToValueTask), BindingFlags.NonPublic | BindingFlags.Static) + .MakeGenericMethod(type.GetGenericArguments()); + + return Expression.Lambda(Expression.Call(conversionMethod, closedGenericTaskParam), closedGenericTaskParam); + } + } + + private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncType) => + IsCoerceableFSharpType(possibleFSharpAsyncType, FSharpAsyncGenericTypeName); + + private static bool IsFSharpUnit(Type possibleFSharpUnitType) => + IsCoerceableFSharpType(possibleFSharpUnitType, FSharpUnitTypeName); + + private static bool IsCoerceableFSharpType(Type possibleFSharpType, string coerceableFSharpTypeName) { - var typeFullName = possibleFSharpAsyncGenericType?.FullName; - if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal)) + var typeFullName = possibleFSharpType?.FullName; + if (!string.Equals(typeFullName, coerceableFSharpTypeName, StringComparison.Ordinal)) { return false; } @@ -102,15 +186,15 @@ private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGeneric { if (_fsharpCoreAssembly != null) { - // Since we've already found the real FSharpAsync.Core assembly, we just have - // to check that the supplied FSharpAsync`1 type is the one from that assembly. - return possibleFSharpAsyncGenericType.Assembly == _fsharpCoreAssembly; + // Since we've already found the real FSharp.Core assembly, we just have + // to check that the supplied type is the one from that assembly. + return possibleFSharpType.Assembly == _fsharpCoreAssembly; } else { - // We'll keep trying to find the FSharp types/values each time any type called - // FSharpAsync`1 is supplied. - return TryPopulateFSharpValueCaches(possibleFSharpAsyncGenericType); + // We'll keep trying to find the F# types/values each time any type + // with a name of interest is supplied. + return TryPopulateFSharpValueCaches(possibleFSharpType); } } } @@ -118,11 +202,12 @@ private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGeneric [UnconditionalSuppressMessage("Trimmer", "IL2026", Justification = "Reflecting over the async FSharpAsync<> contract")] [UnconditionalSuppressMessage("Trimmer", "IL2055", Justification = "Reflecting over the async FSharpAsync<> contract")] [UnconditionalSuppressMessage("Trimmer", "IL2072", Justification = "Reflecting over the async FSharpAsync<> contract")] - private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGenericType) + private static bool TryPopulateFSharpValueCaches(Type possibleFSharpType) { - var assembly = possibleFSharpAsyncGenericType.Assembly; - var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); - var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync"); + var assembly = possibleFSharpType.Assembly; + var fsharpOptionType = assembly.GetType(FSharpOptionTypeName); + var fsharpAsyncType = assembly.GetType(FSharpAsyncTypeName); + var fsharpAsyncGenericType = assembly.GetType(FSharpAsyncGenericTypeName); if (fsharpOptionType == null || fsharpAsyncType == null) { @@ -149,7 +234,7 @@ private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGeneric { var parameters = candidateMethodInfo.GetParameters(); if (parameters.Length == 3 - && TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType) + && TypesHaveSameIdentity(parameters[0].ParameterType, fsharpAsyncGenericType) && parameters[1].ParameterType == fsharpOptionOfTaskCreationOptionsType && parameters[2].ParameterType == fsharpOptionOfCancellationTokenType) { @@ -169,4 +254,6 @@ private static bool TypesHaveSameIdentity(Type type1, Type type2) && string.Equals(type1.Namespace, type2.Namespace, StringComparison.Ordinal) && string.Equals(type1.Name, type2.Name, StringComparison.Ordinal); } + + private static async ValueTask ConvertValueTaskOfTToValueTask(ValueTask valueTask) => await valueTask; } From 5904ea522ea5b5c1015035cbfeeb2e8e35142a2b Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Mar 2023 13:30:11 -0400 Subject: [PATCH 14/20] =?UTF-8?q?Add=20tests=20for=20unit=20=E2=86=92=20vo?= =?UTF-8?q?id=20awaitable=20conversions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/RequestDelegateFactoryTests.cs | 32 +++++++- .../CompileTimeCreationTests.cs | 42 +++++++++++ .../RuntimeCreationTests.cs | 24 +++++- .../Microsoft.AspNetCore.OpenApi.Tests.csproj | 3 +- src/OpenApi/test/OpenApiGeneratorTests.cs | 9 +++ .../Shared.Tests/ObjectMethodExecutorTest.cs | 74 +++++++++++++++++-- 6 files changed, 173 insertions(+), 11 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index e3c9eaf4b7de..621395bdd2d2 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -5988,11 +5988,26 @@ ValueTask ValueTaskMethod() return ValueTask.CompletedTask; } + ValueTask ValueTaskOfUnitMethod() + { + return ValueTask.FromResult(default(FSharp.Core.Unit)!); + } + Task TaskMethod() { return Task.CompletedTask; } + Task TaskOfUnitMethod() + { + return Task.FromResult(default(FSharp.Core.Unit)!); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfUnitMethod() + { + return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(FSharp.Core.Unit)!); + } + async ValueTask ValueTaskWithYieldMethod() { await Task.Yield(); @@ -6003,13 +6018,28 @@ async Task TaskWithYieldMethod() await Task.Yield(); } + FSharp.Control.FSharpAsync FSharpAsyncOfUnitWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return default!; + } + } + return new object[][] { new object[] { (Action)VoidMethod }, new object[] { (Func)ValueTaskMethod }, + new object[] { (Func>)ValueTaskOfUnitMethod }, new object[] { (Func)TaskMethod }, + new object[] { (Func>)TaskOfUnitMethod }, + new object[] { (Func>)FSharpAsyncOfUnitMethod }, new object[] { (Func)ValueTaskWithYieldMethod }, - new object[] { (Func)TaskWithYieldMethod} + new object[] { (Func)TaskWithYieldMethod}, + new object[] { (Func>)FSharpAsyncOfUnitWithYieldMethod } }; } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs index 1b09a082e459..3a87846ba804 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs @@ -124,4 +124,46 @@ public async Task MapAction_WarnsForUnsupportedRouteVariable() await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, "Hello world!"); } + + [Theory] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return("Hello"));""")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new Todo { Name = "Hello" }));""")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(TypedResults.Ok(new Todo { Name = "Hello" })));""")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(Microsoft.FSharp.Core.Unit)!));""")] + public async Task MapAction_NoParam_FSharpAsyncReturn_NotCoercedToTaskAtCompileTime(string source) + { + var (result, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(result, endpointModel => + { + Assert.Equal("/", endpointModel.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.False(endpointModel.Response.IsAwaitable); + }); + + var httpContext = CreateHttpContext(); + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, expectedBody: "{}"); + } + + [Theory] + [InlineData("""app.MapGet("/", () => Task.FromResult(default(Microsoft.FSharp.Core.Unit)!));""")] + [InlineData("""app.MapGet("/", () => ValueTask.FromResult(default(Microsoft.FSharp.Core.Unit)!));""")] + public async Task MapAction_NoParam_TaskLikeOfUnitReturn_NotConvertedToVoidReturningAtCompileTime(string source) + { + var (result, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(result, endpointModel => + { + Assert.Equal("/", endpointModel.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + }); + + var httpContext = CreateHttpContext(); + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, expectedBody: "null"); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs index d2cc624aebab..ca824eda4104 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs @@ -26,8 +26,7 @@ public async Task MapAction_BindAsync_WithWrongType_IsNotUsed(string bindAsyncTy [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return("Hello"));""", "Hello")] [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new Todo { Name = "Hello" }));""", """{"id":0,"name":"Hello","isComplete":false}""")] [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(TypedResults.Ok(new Todo { Name = "Hello" })));""", """{"id":0,"name":"Hello","isComplete":false}""")] - [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(Microsoft.FSharp.Core.Unit)));""", "null")] - public async Task MapAction_RuntimeCreation_FSharpAsync_IsAwaitable(string source, string expectedBody) + public async Task MapAction_FSharpAsyncReturn_IsAwaitable(string source, string expectedBody) { var (result, compilation) = await RunGeneratorAsync(source); var endpoint = GetEndpointFromCompilation(compilation); @@ -43,4 +42,25 @@ public async Task MapAction_RuntimeCreation_FSharpAsync_IsAwaitable(string sourc await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, expectedBody); } + + [Theory] + [InlineData("""app.MapGet("/", () => Task.FromResult(default(Microsoft.FSharp.Core.Unit)!));""")] + [InlineData("""app.MapGet("/", () => ValueTask.FromResult(default(Microsoft.FSharp.Core.Unit)!));""")] + [InlineData("""app.MapGet("/", () => Microsoft.FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(Microsoft.FSharp.Core.Unit)!));""")] + public async Task MapAction_AwaitableOfUnitReturn_ConvertedToVoidReturning(string source) + { + var (result, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + VerifyStaticEndpointModel(result, endpointModel => + { + Assert.Equal("/", endpointModel.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + }); + + var httpContext = CreateHttpContext(); + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, expectedBody: ""); + } } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj index efa9290e2325..507534ae26c1 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests.csproj @@ -1,10 +1,11 @@ - + $(DefaultNetCoreTargetFramework) + diff --git a/src/OpenApi/test/OpenApiGeneratorTests.cs b/src/OpenApi/test/OpenApiGeneratorTests.cs index d93e86ea7d2e..ce994629e758 100644 --- a/src/OpenApi/test/OpenApiGeneratorTests.cs +++ b/src/OpenApi/test/OpenApiGeneratorTests.cs @@ -156,6 +156,12 @@ static void AssertJsonResponse(OpenApiOperation operation, string expectedType) AssertJsonResponse(GetOpenApiOperation(() => new InferredJsonClass()), "object"); AssertJsonResponse(GetOpenApiOperation(() => (IInferredJsonInterface)null), "object"); + AssertJsonResponse(GetOpenApiOperation(() => Task.FromResult(new InferredJsonClass())), "object"); + AssertJsonResponse(GetOpenApiOperation(() => Task.FromResult((IInferredJsonInterface)null)), "object"); + AssertJsonResponse(GetOpenApiOperation(() => ValueTask.FromResult(new InferredJsonClass())), "object"); + AssertJsonResponse(GetOpenApiOperation(() => ValueTask.FromResult((IInferredJsonInterface)null)), "object"); + AssertJsonResponse(GetOpenApiOperation(() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new InferredJsonClass())), "object"); + AssertJsonResponse(GetOpenApiOperation(() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return((IInferredJsonInterface)null)), "object"); } [Fact] @@ -181,7 +187,10 @@ static void AssertVoid(OpenApiOperation operation) AssertVoid(GetOpenApiOperation(() => { })); AssertVoid(GetOpenApiOperation(() => Task.CompletedTask)); + AssertVoid(GetOpenApiOperation(() => Task.FromResult(default(FSharp.Core.Unit)))); AssertVoid(GetOpenApiOperation(() => new ValueTask())); + AssertVoid(GetOpenApiOperation(() => ValueTask.FromResult(default(FSharp.Core.Unit)))); + AssertVoid(GetOpenApiOperation(() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(FSharp.Core.Unit)))); } [Fact] diff --git a/src/Shared/test/Shared.Tests/ObjectMethodExecutorTest.cs b/src/Shared/test/Shared.Tests/ObjectMethodExecutorTest.cs index 68f511fada7d..579478a530d1 100644 --- a/src/Shared/test/Shared.Tests/ObjectMethodExecutorTest.cs +++ b/src/Shared/test/Shared.Tests/ObjectMethodExecutorTest.cs @@ -125,10 +125,15 @@ await Assert.ThrowsAsync( new object[] { parameter })); } - [Fact] - public async Task ExecuteValueMethodWithReturnVoidThrowsExceptionAsync() - { - var executor = GetExecutorForMethod("ValueMethodWithReturnVoidThrowsExceptionAsync"); + [Theory] + [InlineData(nameof(TestObject.ValueMethodWithReturnVoidThrowsExceptionAsync))] + [InlineData(nameof(TestObject.ValueMethodWithReturnVoidValueTaskThrowsExceptionAsync))] + [InlineData(nameof(TestObject.ValueMethodWithReturnUnitThrowsExceptionAsync))] + [InlineData(nameof(TestObject.ValueMethodWithReturnValueTaskOfUnitThrowsExceptionAsync))] + [InlineData(nameof(TestObject.ValueMethodWithReturnFSharpAsyncOfUnitThrowsExceptionAsync))] + public async Task ExecuteValueMethodWithReturnVoidThrowsExceptionAsync(string method) + { + var executor = GetExecutorForMethod(method); var parameter = new TestObject(); Assert.True(executor.IsMethodAsync); await Assert.ThrowsAsync( @@ -219,11 +224,16 @@ public async Task TargetMethodReturningCustomAwaitableOfValueType_CanInvokeViaEx Assert.Equal(579, (int)result); } - [Fact] - public async Task TargetMethodReturningAwaitableOfVoidType_CanInvokeViaExecuteAsync() + [Theory] + [InlineData(nameof(TestObject.VoidValueMethodAsync))] + [InlineData(nameof(TestObject.VoidValueTaskMethodAsync))] + [InlineData(nameof(TestObject.TaskOfUnitMethodAsync))] + [InlineData(nameof(TestObject.ValueTaskOfUnitMethodAsync))] + [InlineData(nameof(TestObject.FSharpAsyncOfUnitMethod))] + public async Task TargetMethodReturningAwaitableOfVoidType_CanInvokeViaExecuteAsync(string method) { // Arrange - var executor = GetExecutorForMethod("VoidValueMethodAsync"); + var executor = GetExecutorForMethod(method); // Act var result = await executor.ExecuteAsync(_targetObject, new object[] { 123 }); @@ -447,6 +457,27 @@ public async Task VoidValueMethodAsync(int i) { await ValueMethodAsync(3, 4); } + + public Task TaskOfUnitMethodAsync(int i) + { + return Task.FromResult(default(Unit)); + } + + public async ValueTask VoidValueTaskMethodAsync(int i) + { + await ValueMethodAsync(3, 4); + } + + public ValueTask ValueTaskOfUnitMethodAsync(int i) + { + return ValueTask.FromResult(default(Unit)); + } + + public FSharpAsync FSharpAsyncOfUnitMethod() + { + return ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(Unit)); + } + public Task ValueMethodWithReturnTypeAsync(int i) { return Task.FromResult(new TestObject() { value = "Hello" }); @@ -458,6 +489,35 @@ public async Task ValueMethodWithReturnVoidThrowsExceptionAsync(TestObject i) throw new NotImplementedException("Not Implemented Exception"); } + public async Task ValueMethodWithReturnUnitThrowsExceptionAsync(TestObject i) + { + await Task.FromResult(default(Unit)); + throw new NotImplementedException("Not Implemented Exception"); + } + + public async ValueTask ValueMethodWithReturnVoidValueTaskThrowsExceptionAsync(TestObject i) + { + await ValueTask.CompletedTask; + throw new NotImplementedException("Not Implemented Exception"); + } + + public async ValueTask ValueMethodWithReturnValueTaskOfUnitThrowsExceptionAsync(TestObject i) + { + await ValueTask.FromResult(default(Unit)); + throw new NotImplementedException("Not Implemented Exception"); + } + + public FSharpAsync ValueMethodWithReturnFSharpAsyncOfUnitThrowsExceptionAsync(TestObject i) + { + return FSharpAsync.AwaitTask(Run()); + + static Task Run() + { + Task.FromResult(default(Unit)); + throw new NotImplementedException("Not Implemented Exception"); + } + } + public async Task ValueMethodWithReturnTypeThrowsExceptionAsync(TestObject i) { await Task.CompletedTask; From 03e61a785c89077a0e994b9a7437883b7c709819 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Mar 2023 13:39:34 -0400 Subject: [PATCH 15/20] Make consistent --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index c7f57b1dba35..704a73219061 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1019,9 +1019,13 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } else if (CoercedAwaitableInfo.IsTypeAwaitable(returnType, out var coercedAwaitableInfo)) { - if (coercedAwaitableInfo.CoercerExpression is LambdaExpression coercerExpression) + if (coercedAwaitableInfo.CoercerResultType is { } coercedType) + { + returnType = coercedType; + } + + if (coercedAwaitableInfo.CoercerExpression is { } coercerExpression) { - returnType = coercerExpression.ReturnType; methodCall = Expression.Invoke(coercerExpression, methodCall); } From dc353d4fffad3fb54a07f9190996bc4b84212553 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 26 Mar 2023 14:02:23 -0400 Subject: [PATCH 16/20] Don't need --- .../RuntimeCreationTests.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs index ca824eda4104..1d3d01d62bf4 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs @@ -31,13 +31,6 @@ public async Task MapAction_FSharpAsyncReturn_IsAwaitable(string source, string var (result, compilation) = await RunGeneratorAsync(source); var endpoint = GetEndpointFromCompilation(compilation); - VerifyStaticEndpointModel(result, endpointModel => - { - Assert.Equal("/", endpointModel.RoutePattern); - Assert.Equal("MapGet", endpointModel.HttpMethod); - Assert.True(endpointModel.Response.IsAwaitable); - }); - var httpContext = CreateHttpContext(); await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, expectedBody); @@ -52,13 +45,6 @@ public async Task MapAction_AwaitableOfUnitReturn_ConvertedToVoidReturning(strin var (result, compilation) = await RunGeneratorAsync(source); var endpoint = GetEndpointFromCompilation(compilation); - VerifyStaticEndpointModel(result, endpointModel => - { - Assert.Equal("/", endpointModel.RoutePattern); - Assert.Equal("MapGet", endpointModel.HttpMethod); - Assert.True(endpointModel.Response.IsAwaitable); - }); - var httpContext = CreateHttpContext(); await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, expectedBody: ""); From 3bbe1e7c77136e5d762c7430c0ad740f25a15dff Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sun, 11 Jun 2023 18:41:45 -0400 Subject: [PATCH 17/20] Handle boxed IResult --- .../src/RequestDelegateFactory.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 9aa66eb34fc2..7a72f7dff5e7 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1055,6 +1055,14 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, methodCall, HttpContextExpr); } + else if (typeArg == typeof(object)) + { + return Expression.Call( + ExecuteTaskOfObjectMethod, + methodCall, + HttpContextExpr, + Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + } else { var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg); @@ -1095,6 +1103,14 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, methodCall, HttpContextExpr); } + else if (typeArg == typeof(object)) + { + return Expression.Call( + ExecuteValueTaskOfObjectMethod, + methodCall, + HttpContextExpr, + Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo))); + } else { var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg); From 3fea9df18cdc171d51c4412643ca13eb0a0a245c Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sat, 17 Jun 2023 10:54:49 -0400 Subject: [PATCH 18/20] Simplify --- .../test/RequestDelegateGenerator/CompileTimeCreationTests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs index 82d83c1329aa..758402e1153b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs @@ -50,7 +50,6 @@ public async Task MapAction_NoParam_FSharpAsyncReturn_NotCoercedToTaskAtCompileT VerifyStaticEndpointModel(result, endpointModel => { - Assert.Equal("/", endpointModel.RoutePattern); Assert.Equal("MapGet", endpointModel.HttpMethod); Assert.False(endpointModel.Response.IsAwaitable); }); @@ -70,7 +69,6 @@ public async Task MapAction_NoParam_TaskLikeOfUnitReturn_NotConvertedToVoidRetur VerifyStaticEndpointModel(result, endpointModel => { - Assert.Equal("/", endpointModel.RoutePattern); Assert.Equal("MapGet", endpointModel.HttpMethod); Assert.True(endpointModel.Response.IsAwaitable); }); @@ -79,6 +77,4 @@ public async Task MapAction_NoParam_TaskLikeOfUnitReturn_NotConvertedToVoidRetur await endpoint.RequestDelegate(httpContext); await VerifyResponseBodyAsync(httpContext, expectedBody: "null"); } - - } From 2111eb2370a05c7981dba211c5e721825e917f9f Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Sat, 17 Jun 2023 10:55:24 -0400 Subject: [PATCH 19/20] Add runtime tests back --- .../test/RequestDelegateFactoryTests.cs | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 7d3e3f41517c..d05c9ff7303a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -1420,10 +1420,12 @@ public static IEnumerable CustomResults CustomResult TestAction() => new CustomResult(resultString); Task TaskTestAction() => Task.FromResult(new CustomResult(resultString)); ValueTask ValueTaskTestAction() => ValueTask.FromResult(new CustomResult(resultString)); + FSharp.Control.FSharpAsync FSharpAsyncTestAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CustomResult(resultString)); static CustomResult StaticTestAction() => new CustomResult("Still not enough tests!"); static Task StaticTaskTestAction() => Task.FromResult(new CustomResult("Still not enough tests!")); static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); + static FSharp.Control.FSharpAsync StaticFSharpAsyncTestAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CustomResult("Still not enough tests!")); // Object return type where the object is IResult static object StaticResultAsObject() => new CustomResult("Still not enough tests!"); @@ -1431,28 +1433,35 @@ public static IEnumerable CustomResults // Task return type static Task StaticTaskOfIResultAsObject() => Task.FromResult(new CustomResult("Still not enough tests!")); static ValueTask StaticValueTaskOfIResultAsObject() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); + static FSharp.Control.FSharpAsync StaticFSharpAsyncOfIResultAsObject() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new CustomResult("Still not enough tests!")); StructResult TestStructAction() => new StructResult(resultString); Task TaskTestStructAction() => Task.FromResult(new StructResult(resultString)); ValueTask ValueTaskTestStructAction() => ValueTask.FromResult(new StructResult(resultString)); + FSharp.Control.FSharpAsync FSharpAsyncTestStructAction() => FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new StructResult(resultString)); return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction}, new object[] { (Func>)ValueTaskTestAction}, + new object[] { (Func>)FSharpAsyncTestAction }, + new object[] { (Func)StaticTestAction}, new object[] { (Func>)StaticTaskTestAction}, new object[] { (Func>)StaticValueTaskTestAction}, + new object[] { (Func>)StaticFSharpAsyncTestAction }, new object[] { (Func)StaticResultAsObject}, new object[] { (Func>)StaticTaskOfIResultAsObject}, new object[] { (Func>)StaticValueTaskOfIResultAsObject}, + new object[] { (Func>)StaticFSharpAsyncOfIResultAsObject}, new object[] { (Func)TestStructAction }, new object[] { (Func>)TaskTestStructAction }, new object[] { (Func>)ValueTaskTestStructAction }, + new object[] { (Func>)FSharpAsyncTestStructAction }, }; } } @@ -2218,6 +2227,169 @@ public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithProvided Assert.Equal("Hello, TestName!", decodedResponseBody); } + public static object[][] FSharpAsyncOfTMethods + { + get + { + FSharp.Control.FSharpAsync FSharpAsyncOfTMethod() + { + return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return("foo"); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfTWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return "foo"; + } + } + + FSharp.Control.FSharpAsync FSharpAsyncOfObjectWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return "foo"; + } + } + + return new object[][] + { + new object[] { (Func>)FSharpAsyncOfTMethod }, + new object[] { (Func>)FSharpAsyncOfTWithYieldMethod }, + new object[] { (Func>)FSharpAsyncOfObjectWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(FSharpAsyncOfTMethods))] + public async Task CanInvokeFilter_OnFSharpAsyncOfTReturningHandler(Delegate @delegate) + { + // Arrange + var responseBodyStream = new MemoryStream(); + var httpContext = CreateHttpContext(); + httpContext.Response.Body = responseBodyStream; + + // Act + var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + { + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() + { + (routeHandlerContext, next) => async (context) => + { + return await next(context); + } + }), + }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal("foo", decodedResponseBody); + } + + public static object[][] VoidReturningMethods + { + get + { + void VoidMethod() { } + + ValueTask ValueTaskMethod() + { + return ValueTask.CompletedTask; + } + + ValueTask ValueTaskOfUnitMethod() + { + return ValueTask.FromResult(default(FSharp.Core.Unit)!); + } + + Task TaskMethod() + { + return Task.CompletedTask; + } + + Task TaskOfUnitMethod() + { + return Task.FromResult(default(FSharp.Core.Unit)!); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfUnitMethod() + { + return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(default(FSharp.Core.Unit)!); + } + + async ValueTask ValueTaskWithYieldMethod() + { + await Task.Yield(); + } + + async Task TaskWithYieldMethod() + { + await Task.Yield(); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfUnitWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return default!; + } + } + + return new object[][] + { + new object[] { (Action)VoidMethod }, + new object[] { (Func)ValueTaskMethod }, + new object[] { (Func>)ValueTaskOfUnitMethod }, + new object[] { (Func)TaskMethod }, + new object[] { (Func>)TaskOfUnitMethod }, + new object[] { (Func>)FSharpAsyncOfUnitMethod }, + new object[] { (Func)ValueTaskWithYieldMethod }, + new object[] { (Func)TaskWithYieldMethod}, + new object[] { (Func>)FSharpAsyncOfUnitWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(VoidReturningMethods))] + public async Task CanInvokeFilter_OnVoidReturningHandler(Delegate @delegate) + { + // Arrange + var responseBodyStream = new MemoryStream(); + var httpContext = CreateHttpContext(); + httpContext.Response.Body = responseBodyStream; + + // Act + var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + { + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() + { + (routeHandlerContext, next) => async (context) => + { + return await next(context); + } + }), + }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(String.Empty, decodedResponseBody); + } + [Fact] public async Task CanInvokeFilter_OnTaskModifyingHttpContext() { @@ -2255,6 +2427,91 @@ async Task HandlerWithTaskAwait(HttpContext c) Assert.Equal(string.Empty, decodedResponseBody); } + public static object[][] TasksOfTypesMethods + { + get + { + ValueTask ValueTaskOfStructMethod() + { + return ValueTask.FromResult(new TodoStruct { Name = "Test todo" }); + } + + async ValueTask ValueTaskOfStructWithYieldMethod() + { + await Task.Yield(); + return new TodoStruct { Name = "Test todo" }; + } + + Task TaskOfStructMethod() + { + return Task.FromResult(new TodoStruct { Name = "Test todo" }); + } + + async Task TaskOfStructWithYieldMethod() + { + await Task.Yield(); + return new TodoStruct { Name = "Test todo" }; + } + + FSharp.Control.FSharpAsync FSharpAsyncOfStructMethod() + { + return FSharp.Core.ExtraTopLevelOperators.DefaultAsyncBuilder.Return(new TodoStruct { Name = "Test todo" }); + } + + FSharp.Control.FSharpAsync FSharpAsyncOfStructWithYieldMethod() + { + return FSharp.Control.FSharpAsync.AwaitTask(Yield()); + + async Task Yield() + { + await Task.Yield(); + return new TodoStruct { Name = "Test todo" }; + } + } + + return new object[][] + { + new object[] { (Func>)ValueTaskOfStructMethod }, + new object[] { (Func>)ValueTaskOfStructWithYieldMethod }, + new object[] { (Func>)TaskOfStructMethod }, + new object[] { (Func>)TaskOfStructWithYieldMethod }, + new object[] { (Func>)FSharpAsyncOfStructMethod }, + new object[] { (Func>)FSharpAsyncOfStructWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(TasksOfTypesMethods))] + public async Task CanInvokeFilter_OnHandlerReturningTasksOfStruct(Delegate @delegate) + { + // Arrange + var responseBodyStream = new MemoryStream(); + var httpContext = CreateHttpContext(); + httpContext.Response.Body = responseBodyStream; + + // Act + var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + { + EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() + { + (routeHandlerContext, next) => async (context) => + { + return await next(context); + } + }), + }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + Assert.Equal("Test todo", deserializedResponseBody.Name); + } + [Fact] public void Create_DoesNotAddDelegateMethodInfo_AsMetadata() { From 801d429e5f2d45549d99f46de06b8289a52cd7a5 Mon Sep 17 00:00:00 2001 From: Brian Rourke Boll Date: Wed, 16 Aug 2023 12:35:03 -0400 Subject: [PATCH 20/20] Use affirmative form --- .../ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs index f270977006b0..e3f977dcc2cb 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -147,7 +147,7 @@ public static bool TryBuildCoercerFromUnitAwaitableToVoidAwaitable( _ => default }; - return (nonGenericAwaitableType, coercerExpression) is not (null, null); + return (nonGenericAwaitableType, coercerExpression) is ({ }, { }); static Expression MakeTaskOfUnitToTaskExpression(Type type) {