Skip to content

Commit

Permalink
Add support for F# Async<'T> as an awaitable type in minimal APIs (#…
Browse files Browse the repository at this point in the history
…46898)

* Remove unneeded unboxing/boxing

* Add doc comment

* Remove unused using

* Convert F# Async<> to Task<> in request delegates

* Use `CoercedAwaitableInfo` to convert `FSharp.Control.FSharpAsync<T>`
  values to `System.Threading.Tasks.Task<TResult>` when building request
  delegates and populate endpoint metadata accordingly.

* Add tests for F# async request delegate support

* 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.

* Actually remove extra convert call

* Shrink diff

* Use CoercedAwaitableInfo in OpenApi gen

* Use CoercedAwaitableInfo in EndpointMetadataApiDescriptionProvider

* Add FSharpAsync tests for EndpointMetadataApiDescriptionProvider

* Add runtime creation test for FSharpAsync

* Convert unit awaitables to void awaitables

* FSharpAsync<unit> → Task

* Task<unit> → Task

* ValueTask<unit> → ValueTask

* Add tests for unit → void awaitable conversions

* Make consistent

* Don't need

* Handle boxed IResult

* Simplify

* Add runtime tests back

* Use affirmative form

---------

Co-authored-by: Safia Abdalla <[email protected]>
  • Loading branch information
brianrourkeboll and captainsafia authored Nov 27, 2023
1 parent 97bea8d commit 444aa9e
Show file tree
Hide file tree
Showing 15 changed files with 703 additions and 101 deletions.
108 changes: 66 additions & 42 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object?>))
{
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<object?>))
{
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<object?> ValueTaskOfTToValueTaskOfObject<T>(ValueTask<T> valueTask)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1057,8 +1055,18 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
HttpContextExpr,
Expression.Constant(factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeof(object)), typeof(JsonTypeInfo<object>)));
}
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;
Expand Down Expand Up @@ -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<object>)));
}
else
{
var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg);
Expand Down Expand Up @@ -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<object>)));
}
else
{
var jsonTypeInfo = factoryContext.JsonSerializerOptions.GetReadOnlyTypeInfo(typeArg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</ItemGroup>

<ItemGroup>
<Reference Include="FSharp.Core" />
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Results" />
Expand Down
Loading

0 comments on commit 444aa9e

Please sign in to comment.