Considere o caso de uma biblioteca destinada ao uso com outros projetos, que podem ser aplicativos de console, outras bibliotecas de classes ou aplicativos com UI. Esta biblioteca permite ao usuário enviar comandos e receber respostas de um terminal remoto, mas também pode receber mensagens de eventos do terminal remoto. É um caso de uso válido que o usuário possa enviar um comando em resposta a um evento remoto. O mecanismo de transporte utiliza async/await
métodos para transferir dados de e para o terminal remoto (por exemplo, usando um WebSocket). Em um aplicativo .NET "clássico", eu poderia projetar uma API como a seguinte ( Nota importante: o código apresentado aqui não é canônico, provavelmente não será compilado como está e foi escrito apenas para fins de discussão):
private Command currentCommand;
public event EventHandler<EventReceivedEventArgs>? EventReceived;
public CommandResponse ExecuteCommand(CommandParameters parameters)
{
currentCommand = transport.SendData(parameters.Serialize());
return currentCommand.WaitForCommandResponse();
}
protected void OnEventReceived(EventData eventData)
{
if (this.EventReceived is not null)
{
this.EventReceived(this, new EventReceivedEventArgs(eventData));
}
}
private void DataReceiver()
{
// Example only. In a real app, there would be some terminating
// condition here.
while (true)
{
// For discussion purposes, assume this an API that returns data
// from the remote end. How it works and how it is parsed can be
// considered an implementation detail.
byte[] receivedData = transport.ReceiveData();
ParsedData parsed = Parse(receivedData);
if (parsed is CommandResponse)
{
this.currentCommand.SetResponse(parsed);
}
if (parsed is EventData)
{
this.OnEventReceived(parsed);
}
}
}
No entanto, dado que ouvi repetidamente (e hoje, de forma um tanto incisiva), "eventos .NET não funcionam com async/await", "Não use async void
nunca" e "Não misture síncrono e assíncrono código." Então, dada a async/await
forma da API, parece:
private Command currentCommand;
// This, right here, I'm told, is bad, bad, bad. Totally suspect.
// Poor API design choice. Don't do it. This API, and it's caller
// below are presented merely as placeholders for discussion.
public event EventHandler<EventReceivedEventArgs>? EventReceived;
public async Task<CommandResponse> ExecuteCommandAsync(CommandParameters parameters)
{
await transport.SendCommandAsync(parameters.Serialize());
return await transport.WaitForCommandResponse();
}
protected void OnEventReceived(EventData eventData)
{
if (this.EventReceived is not null)
{
this.EventReceived(this, new EventReceivedEventArgs(eventData));
}
}
private async Task DataReceiver()
{
while (true)
{
await byte[] receivedData = transport.ReceiveData();
ParsedData parsed = Parse(receivedData);
if (parsed is CommandResponse)
{
this.currentCommand.SetResponse(parsed);
}
if (parsed is EventData)
{
this.OnEventReceived(parsed);
}
}
return Task.CompletedTask;
}
Eu entendo perfeitamente por que isso não é ideal. O manipulador de eventos do consumidor bloqueará o método de produção. Pode travar ou durar muito tempo. Pode ser que jogue. Há uma infinidade de coisas que podem ser problemáticas com esse estilo de API.
Mas aqui está a questão operativa: Qual é a alternativa? O que a comunidade de desenvolvedores .NET recomenda como forma de apresentar esse tipo de recurso ao usuário em vez de usar eventos .NET?