diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 82a882f7025a..19db07bcc737 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -507,34 +507,45 @@ 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.CoercerResultType is { } coercedType) + { + returnType = coercedType; + } + + if (coercedAwaitableInfo.CoercerExpression is { } coercerExpression) + { + 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) @@ -994,22 +1005,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. @@ -1057,8 +1055,18 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, HttpContextExpr, 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.CoercerResultType is { } coercedType) + { + returnType = coercedType; + } + + if (coercedAwaitableInfo.CoercerExpression is { } coercerExpression) + { + methodCall = Expression.Invoke(coercerExpression, methodCall); + } + if (returnType == typeof(Task)) { return methodCall; @@ -1089,6 +1097,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); @@ -1129,6 +1145,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); 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 9a0b42f4a7fa..686ab34dd28a 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 0de35134714b..f462a83878f1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -1421,10 +1421,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!"); @@ -1432,28 +1434,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 }, }; } } @@ -2219,6 +2228,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() { @@ -2256,6 +2428,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() { @@ -2394,6 +2651,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() { @@ -2460,6 +2730,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() { @@ -2672,6 +2964,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() { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs index 2ea6ce459d5f..5687acc891d5 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/CompileTimeCreationTests.cs @@ -557,6 +557,46 @@ public async Task SupportsHandlersWithSameSignatureButDifferentParameterNamesFro Assert.Equal(@"Implicit body inferred for parameter ""todo1"" but no body was provided. Did you mean to use a Service instead?", log2.Message); } + [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("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("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + }); + + var httpContext = CreateHttpContext(); + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, expectedBody: "null"); + } + [Fact] public async Task SkipsMapWithIncorrectNamespaceAndAssembly() { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs index 61e8e8145ea1..477d57294e13 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs @@ -26,4 +26,32 @@ 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}""")] + public async Task MapAction_FSharpAsyncReturn_IsAwaitable(string source, string expectedBody) + { + var (result, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + 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)!));""")] + [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); + + var httpContext = CreateHttpContext(); + await endpoint.RequestDelegate(httpContext); + await VerifyResponseBodyAsync(httpContext, expectedBody: ""); + } } diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 4388167511cb..8766b063f412 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. diff --git a/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationModels/EndpointMetadataProviderTest.cs index 2272b3e453a5..535b3e4b5020 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) @@ -132,6 +134,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) @@ -315,6 +319,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; @@ -327,6 +334,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() @@ -335,6 +345,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() @@ -342,6 +355,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 @@ + diff --git a/src/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs index 93b1981e0f97..884427390e22 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)) 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 b8b3b9dba25c..f65f75196eaa 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/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)) 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 95970685f6cb..e3f977dcc2cb 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -3,19 +3,15 @@ #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; /// -/// 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 @@ -25,12 +21,39 @@ 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; 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, + /// 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 and is a generic instantiation of + /// FSharp.Control.FSharpAsync<T>, + /// 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 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.")] public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( Type possibleFSharpAsyncType, @@ -51,33 +74,110 @@ public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single(); awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType); - // coerceToAwaitableExpression = (object fsharpAsync) => + // coerceToAwaitableExpression = (FSharpAsync fsharpAsync) => // { - // return (object)FSharpAsync.StartAsTask( - // (Microsoft.FSharp.Control.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); + + 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) { - var typeFullName = possibleFSharpAsyncGenericType?.FullName; - if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal)) + 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 ({ }, { }); + + 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 = possibleFSharpType?.FullName; + if (!string.Equals(typeFullName, coerceableFSharpTypeName, StringComparison.Ordinal)) { return false; } @@ -86,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); } } } @@ -102,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) { @@ -133,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) { @@ -153,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; } 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;