构建基本的 ASP.NET Core Minimal API 时,我惊讶地发现,如果端点返回Task
,则请求直到Task
完成才会返回:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ICounterService, CounterService>();
var app = builder.Build();
app.MapGet("/counter/value", (ICounterService counter) => counter.Value);
app.MapGet("/counter/incrementnow", (ICounterService counter) => counter.Increment());
app.MapGet("/counter/incrementlater", (ICounterService counter) => Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(_ => counter.Increment()));
app.MapGet("/counter/incrementlaternowait", (ICounterService counter) => { Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(_ => counter.Increment()); return; });
app.Run();
interface ICounterService
{
int Value { get; }
void Increment();
}
class CounterService : ICounterService
{
public int Value { get; private set; }
public void Increment()
{
int previousValue = Value++;
Console.WriteLine($"Incremented from {previousValue} to {Value}.");
}
}
这种行为记录在哪里?
await
如果我在端点中明确执行该任务,我就会期待这种行为async
。
我认为它没有明确的文档记录。文档让它看起来好像只允许返回
Task
(没有提到await
ing 或它是特殊类型):因此,
Task
“任何其他类型T
”都不是string
,也没有实现IResult
,因此它应该作为序列化返回json
,而不是明确地await
被编辑。然而,端点(控制器操作方法或最小 API lambda)被转换成
RequestDelegate
:在
RequestDelegateFactory
将它们插入中间件管道之前,您基本上已经拥有:其神奇之处
RequestDelegateFactory
在于确保上述代码也await
能返回任何可等待类型,例如你的方法/lambda 返回的类型。另请参阅此问题。await
Task
因此
Task
,作为返回类型具有特殊含义——同步返回方法/lambda 的异步版本void
。回到上面的文档在我们的例子中,这个上下文实际上是一个,而
T
不是它自己的类型。typeof(void)
Task
Task<void>
T
我认为这种解释得到了 asp.net 核心团队的证实,他们承认 .NET7+ 过滤器子管道逻辑中存在一个错误(路由处理程序过滤器不处理所有返回类型),因为
Task
对象被包装为普通对象,ValueTask<object>
而不是在普通中间件中等待 -ValueTask<object>
而是等待包装 - 因此可以将其作为json
响应返回,而不是awaiting
它:输出:
错误描述:
因此他们必须改变
RequestDelegateFactory
逻辑来实际执行构造的委托,而不是依赖于管道中的await
顶层-源代码:await
如果我们想返回一个
Task
不await
适合我们的,我们可以使用返回的 lambdaTask<Task>
和一个过滤器。有据可查:你只需稍微挖掘一下。怪搜索引擎推广垃圾内容,压倒真正的内容!
MapGet
是一种扩展方法,其中定义了两种模式public static class EndpointRouteBuilderExtensions
:以及 MSDocs 链接:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions?view=aspnetcore-9.0
您不能返回
Task
。如果您希望在第一个 yield 上返回,请返回null
,而不是返回 Task 到端点。因此,您可以像这样重写代码,等待任务完成:
或者这将在第一个收益即 Task.Delay 时返回):
最小 API 端点委托的行为与经典 MVC 中的控制器操作相同。因此,如果您返回一个,
Task
则意味着您的端点/操作是异步的,并且框架仍需要等待正在进行的工作来确定实际的 HTTP 响应是什么样子。这里的 Minimal API 没什么特别之处。你似乎感到困惑,因为端点委托没有使用
async
和 ,await
但这两者都不是控制器操作中必需的。例如,以下是一个完全正常的异步控制器操作:从语义上讲,这也与以下使用相同
async/await
:但是由于
_weatherService.GetWeatherDataAsync
已经返回了一项任务,您不一定非要async
对其采取行动await
,而可以直接从天气服务返回该任务(尽管这会产生一些可能需要或不需要的效果)。在这两种情况下,操作都会返回一个
Task<string[]>
,这将使 ASP.NET Core await 该操作以等待响应。这async
只会告诉编译器执行它的操作,以便您能够使用await
。但它对运行时来说几乎是不可见的(并且无关紧要)。您在评论中提到了“发射后不管”。如果您确实希望某个端点(最小 API 或控制器操作)在后台运行某些操作的同时也能像“发射后不管”一样运行,则需要通过创建新线程将此操作明确移至后台。这可以很简单,例如
Task.Run
:或者使用你的例子:
请注意,在这种情况下,使用
counter
将会起作用,因为它已注册为单例服务。对于其他服务范围,尤其是范围服务(例如 EF 数据库上下文),这将不起作用,您需要在后台任务中创建自己的服务范围,以防止在端点/操作完成时(早于后台作业完成)处置范围服务。