Skip to content

Commit f24c70e

Browse files
authored
Support late bound results (#34300)
* Support late bound results - Add support for object, Task<object> and ValueTask<object> result types. We can detect that pattern and generate calls into a helper that does a bunch of runtime checks for known result types. - Added tests
1 parent 12c57a5 commit f24c70e

File tree

2 files changed

+121
-6
lines changed

2 files changed

+121
-6
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public static partial class RequestDelegateFactory
2929
private static readonly MethodInfo ExecuteValueTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!;
3030
private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
3131
private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
32+
private static readonly MethodInfo ExecuteObjectReturnMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteObjectReturn), BindingFlags.NonPublic | BindingFlags.Static)!;
3233
private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
3334
private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
3435
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, string, Task>>((response, text) => HttpResponseWritingExtensions.WriteAsync(response, text, default));
@@ -338,6 +339,21 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
338339
{
339340
return Expression.Block(methodCall, CompletedTaskExpr);
340341
}
342+
else if (returnType == typeof(object))
343+
{
344+
return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr);
345+
}
346+
else if (returnType == typeof(ValueTask<object>))
347+
{
348+
// REVIEW: We can avoid this box if it becomes a performance issue
349+
var box = Expression.TypeAs(methodCall, typeof(object));
350+
return Expression.Call(ExecuteObjectReturnMethod, box, HttpContextExpr);
351+
}
352+
else if (returnType == typeof(Task<object>))
353+
{
354+
var convert = Expression.Convert(methodCall, typeof(object));
355+
return Expression.Call(ExecuteObjectReturnMethod, convert, HttpContextExpr);
356+
}
341357
else if (AwaitableInfo.IsTypeAwaitable(returnType, out _))
342358
{
343359
if (returnType == typeof(Task))
@@ -632,6 +648,61 @@ private static MemberInfo GetMemberInfo<T>(Expression<T> expr)
632648
return mc.Member;
633649
}
634650

651+
// The result of the method is null so we fallback to some runtime logic.
652+
// First we check if the result is IResult, Task<IResult> or ValueTask<IResult>. If
653+
// it is, we await if necessary then execute the result.
654+
// Then we check to see if it's Task<object> or ValueTask<object>. If it is, we await
655+
// if necessary and restart the cycle until we've reached a terminal state (unknown type).
656+
// We currently don't handle Task<unknown> or ValueTask<unknown>. We can support this later if this
657+
// ends up being a common scenario.
658+
private static async Task ExecuteObjectReturn(object? obj, HttpContext httpContext)
659+
{
660+
// See if we need to unwrap Task<object> or ValueTask<object>
661+
if (obj is Task<object> taskObj)
662+
{
663+
obj = await taskObj;
664+
}
665+
else if (obj is ValueTask<object> valueTaskObj)
666+
{
667+
obj = await valueTaskObj;
668+
}
669+
else if (obj is Task<IResult?> task)
670+
{
671+
await ExecuteTaskResult(task, httpContext);
672+
return;
673+
}
674+
else if (obj is ValueTask<IResult?> valueTask)
675+
{
676+
await ExecuteValueTaskResult(valueTask, httpContext);
677+
return;
678+
}
679+
else if (obj is Task<string?> taskString)
680+
{
681+
await ExecuteTaskOfString(taskString, httpContext);
682+
return;
683+
}
684+
else if (obj is ValueTask<string?> valueTaskString)
685+
{
686+
await ExecuteValueTaskOfString(valueTaskString, httpContext);
687+
return;
688+
}
689+
690+
// Terminal built ins
691+
if (obj is IResult result)
692+
{
693+
await ExecuteResultWriteResponse(result, httpContext);
694+
}
695+
else if (obj is string stringValue)
696+
{
697+
await httpContext.Response.WriteAsync(stringValue);
698+
}
699+
else
700+
{
701+
// Otherwise, we JSON serialize when we reach the terminal state
702+
await httpContext.Response.WriteAsJsonAsync(obj);
703+
}
704+
}
705+
635706
private static Task ExecuteTask<T>(Task<T> task, HttpContext httpContext)
636707
{
637708
EnsureRequestTaskNotNull(task);
@@ -715,12 +786,12 @@ private static Task ExecuteValueTaskResult<T>(ValueTask<T?> task, HttpContext ht
715786
{
716787
static async Task ExecuteAwaited(ValueTask<T> task, HttpContext httpContext)
717788
{
718-
await EnsureRequestResultNotNull(await task)!.ExecuteAsync(httpContext);
789+
await EnsureRequestResultNotNull(await task).ExecuteAsync(httpContext);
719790
}
720791

721792
if (task.IsCompletedSuccessfully)
722793
{
723-
return EnsureRequestResultNotNull(task.GetAwaiter().GetResult())!.ExecuteAsync(httpContext);
794+
return EnsureRequestResultNotNull(task.GetAwaiter().GetResult()).ExecuteAsync(httpContext);
724795
}
725796

726797
return ExecuteAwaited(task!, httpContext);
@@ -730,12 +801,12 @@ private static async Task ExecuteTaskResult<T>(Task<T?> task, HttpContext httpCo
730801
{
731802
EnsureRequestTaskOfNotNull(task);
732803

733-
await EnsureRequestResultNotNull(await task)!.ExecuteAsync(httpContext);
804+
await EnsureRequestResultNotNull(await task).ExecuteAsync(httpContext);
734805
}
735806

736-
private static async Task ExecuteResultWriteResponse(IResult result, HttpContext httpContext)
807+
private static async Task ExecuteResultWriteResponse(IResult? result, HttpContext httpContext)
737808
{
738-
await EnsureRequestResultNotNull(result)!.ExecuteAsync(httpContext);
809+
await EnsureRequestResultNotNull(result).ExecuteAsync(httpContext);
739810
}
740811

741812
private class FactoryContext

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ public void NonStaticTestAction(HttpContext httpContext)
144144

145145
[Fact]
146146
public async Task NonStaticMethodInfoOverloadWorksWithBasicReflection()
147-
148147
{
149148
var methodInfo = typeof(TestNonStaticActionClass).GetMethod(
150149
nameof(TestNonStaticActionClass.NonStaticTestAction),
@@ -1026,6 +1025,21 @@ public static IEnumerable<object[]> CustomResults
10261025
static Task<CustomResult> StaticTaskTestAction() => Task.FromResult(new CustomResult("Still not enough tests!"));
10271026
static ValueTask<CustomResult> StaticValueTaskTestAction() => ValueTask.FromResult(new CustomResult("Still not enough tests!"));
10281027

1028+
// Object return type where the object is IResult
1029+
static object StaticResultAsObject() => new CustomResult("Still not enough tests!");
1030+
static object StaticResultAsTaskObject() => Task.FromResult<object>(new CustomResult("Still not enough tests!"));
1031+
static object StaticResultAsValueTaskObject() => ValueTask.FromResult<object>(new CustomResult("Still not enough tests!"));
1032+
1033+
// Object return type where the object is Task<IResult>
1034+
static object StaticResultAsTaskIResult() => Task.FromResult<IResult>(new CustomResult("Still not enough tests!"));
1035+
1036+
// Object return type where the object is ValueTask<IResult>
1037+
static object StaticResultAsValueTaskIResult() => ValueTask.FromResult<IResult>(new CustomResult("Still not enough tests!"));
1038+
1039+
// Task<object> return type
1040+
static Task<object> StaticTaskOfIResultAsObject() => Task.FromResult<object>(new CustomResult("Still not enough tests!"));
1041+
static ValueTask<object> StaticValueTaskOfIResultAsObject() => ValueTask.FromResult<object>(new CustomResult("Still not enough tests!"));
1042+
10291043
return new List<object[]>
10301044
{
10311045
new object[] { (Func<CustomResult>)TestAction },
@@ -1034,6 +1048,16 @@ public static IEnumerable<object[]> CustomResults
10341048
new object[] { (Func<CustomResult>)StaticTestAction},
10351049
new object[] { (Func<Task<CustomResult>>)StaticTaskTestAction},
10361050
new object[] { (Func<ValueTask<CustomResult>>)StaticValueTaskTestAction},
1051+
1052+
new object[] { (Func<object>)StaticResultAsObject},
1053+
new object[] { (Func<object>)StaticResultAsTaskObject},
1054+
new object[] { (Func<object>)StaticResultAsValueTaskObject},
1055+
1056+
new object[] { (Func<object>)StaticResultAsTaskIResult},
1057+
new object[] { (Func<object>)StaticResultAsValueTaskIResult},
1058+
1059+
new object[] { (Func<Task<object>>)StaticTaskOfIResultAsObject},
1060+
new object[] { (Func<ValueTask<object>>)StaticValueTaskOfIResultAsObject},
10371061
};
10381062
}
10391063
}
@@ -1069,6 +1093,17 @@ public static IEnumerable<object[]> StringResult
10691093
static Task<string> StaticTaskTestAction() => Task.FromResult("String Test");
10701094
static ValueTask<string> StaticValueTaskTestAction() => ValueTask.FromResult("String Test");
10711095

1096+
// Dynamic via object
1097+
static object StaticStringAsObjectTestAction() => "String Test";
1098+
static object StaticTaskStringAsObjectTestAction() => Task.FromResult("String Test");
1099+
static object StaticValueTaskStringAsObjectTestAction() => ValueTask.FromResult("String Test");
1100+
1101+
// Dynamic via Task<object>
1102+
static Task<object> StaticStringAsTaskObjectTestAction() => Task.FromResult<object>("String Test");
1103+
1104+
// Dynamic via ValueTask<object>
1105+
static ValueTask<object> StaticStringAsValueTaskObjectTestAction() => ValueTask.FromResult<object>("String Test");
1106+
10721107
return new List<object[]>
10731108
{
10741109
new object[] { (Func<string>)TestAction },
@@ -1077,6 +1112,15 @@ public static IEnumerable<object[]> StringResult
10771112
new object[] { (Func<string>)StaticTestAction },
10781113
new object[] { (Func<Task<string>>)StaticTaskTestAction },
10791114
new object[] { (Func<ValueTask<string>>)StaticValueTaskTestAction },
1115+
1116+
new object[] { (Func<object>)StaticStringAsObjectTestAction },
1117+
new object[] { (Func<object>)StaticTaskStringAsObjectTestAction },
1118+
new object[] { (Func<object>)StaticValueTaskStringAsObjectTestAction },
1119+
1120+
new object[] { (Func<Task<object>>)StaticStringAsTaskObjectTestAction },
1121+
new object[] { (Func<ValueTask<object>>)StaticStringAsValueTaskObjectTestAction },
1122+
1123+
10801124
};
10811125
}
10821126
}

0 commit comments

Comments
 (0)