Ao criar uma API mÃnima básica do ASP.NET Core, fiquei surpreso com o fato de que, se um endpoint retorna um Task
, a solicitação não retorna até que o Task
seja concluÃdo:
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}.");
}
}
Onde esse comportamento está documentado?
Eu esperaria esse comportamento se eu explicitamente await
editasse a tarefa em um async
endpoint.
Não acho que esteja explicitamente documentado. A documentação faz parecer que é permitido retornar apenas o
Task
(sem menção de queawait
ele está usando ing ou que é um tipo especial):Assim
Task
como "qualquer outro tipoT
", não umstring
, não implementandoIResult
também, então ele deve ser retornado como serializadojson
, e não explicitamenteawait
ed.Entretanto , os endpoints (métodos de ação do controlador ou lambdas de APIs mÃnimas) são convertidos em
RequestDelegate
s:antes de
RequestDelegateFactory
serem conectados ao pipeline de middleware, onde você basicamente tem:Parte da mágica
RequestDelegateFactory
é garantir que o comando acima tambémawait
contenha qualquer tipo aguardável, como o que seu método/lambda está retornando. Veja também esta pergunta .await
Task
Portanto
Task
, como um tipo de retorno tem um significado especial - a versão assÃncrona dosvoid
métodos de retorno sÃncronos/lambdas. Voltando à documentação acimaNo nosso caso
T
istypeof(void)
eTask
is esse contexto é realmente umTask<void>
e não seu próprioT
tipo.Acredito que essa interpretação seja corroborada pelo reconhecimento da equipe principal do asp.net de um bug na lógica do subpipeline do filtro .NET7+ ( os filtros do manipulador de rotas não manipulam todos os tipos de retorno ), no qual o
Task
objeto era encapsulado como um objeto normalValueTask<object>
e não era aguardado mais acima no middleware normal - o encapsulamentoValueTask<object>
era aguardado - então era possÃvel retorná-lo comojson
resposta, em vezawaiting
dele:SaÃda:
Descrição do bug:
então eles tiveram que mudar a
RequestDelegateFactory
lógica para realmente fazer issoawait
no delegado construÃdo, e não depender doawait
próximo nÃvel superior no pipeline - código-fonte :Se quisermos retornar um
Task
que não sejaawait
ed para nós, podemos fazer uso de um lambda que retornaTask<Task>
e um filtro.Está documentado: basta pesquisar um pouco. Culpe os mecanismos de busca por promoverem lixo em vez de conteúdo real!
MapGet
é um método de extensão com dois padrões definidos empublic static class EndpointRouteBuilderExtensions
:E o link do MSDocs:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions?view=aspnetcore-9.0
Você não pode retornar um
Task
. Se quiser um retorno no primeiro yield, retorne umnull
, não uma Task, para o endpoint.Então você pode reescrever seu código assim, aguardando a conclusão da tarefa:
ou isto que retornaria no primeiro rendimento, ou seja, Task.Delay):
Delegados de endpoint de API mÃnima se comportam da mesma forma que ações de controlador no MVC clássico. Portanto, se você estiver retornando um,
Task
isso significa que seu endpoint/ação é assÃncrono e que ainda há trabalho em andamento que precisará ser aguardado pelo framework para descobrir como será a resposta HTTP real.Não há nada de especial na API Minimal aqui. Você parece estar confuso porque o delegado do endpoint não usa
async
e ,await
mas nenhum dos dois é necessário em ações do controlador. Por exemplo, o seguinte é uma ação de controlador assÃncrona perfeitamente adequada:Semanticamente, isso também é o mesmo que o seguinte, que usa
async/await
:Mas como
_weatherService.GetWeatherDataAsync
já retorna uma tarefa, você não precisa necessariamente executar a açãoasync
nelaawait
, mas pode simplesmente retornar a tarefa diretamente do serviço meteorológico (embora isso tenha alguns efeitos que podem ou não ser desejados).Em ambos os casos, a ação retorna um
Task<string[]>
e é isso que fará o ASP.NET Core aguardar a ação para aguardar a resposta. Oasync
apenas dirá ao compilador para fazer sua mágica para que você possa usarawait
. Mas ele é praticamente invisÃvel (e irrelevante) para o tempo de execução.Nos comentários, você mencionou "disparar e esquecer". Se você quiser que um endpoint (API mÃnima ou ação do controlador) se comporte como "disparar e esquecer" enquanto ainda executa algo em segundo plano, precisará mover essa ação explicitamente para segundo plano, gerando uma nova thread. Isso pode ser tão simples quanto usar
Task.Run
:Ou para usar seu exemplo:
Observe que, neste caso, o uso
counter
funcionará, pois está registrado como um serviço singleton. Para outros escopos de serviço, especialmente um serviço com escopo (como um contexto de banco de dados EF), isso não funcionará e você precisará criar seu próprio escopo de serviço dentro da sua tarefa em segundo plano para evitar que o serviço com escopo seja descartado quando o endpoint/ação for concluÃdo (o que ocorre antes da sua tarefa em segundo plano).