Consider the case of a library intended for use with other projects, which could be console applications, other class libraries, or applications with a UI. This library allows the user to send commands to and receive responses from a remote end point, but can also receive event messages from the remote end point. It is a valid use case that the user may send a command in response to a remote event. The transport mechanism uses async/await
methods for transferring data to and from the remote end (e.g., using a WebSocket). In a “classic” .NET application, I might design an API like the following:
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);
}
}
}
However, given that I’ve heard repeatedly (and today, somewhat pointedly), “.NET events don’t work with async/await,” “Don’t use async void
, ever,” and, “Don’t mix synchronous and async code.” So given the async/await
form of the API looks like:
private Command currentCommand;
// This, right here, I'm told, is bad, bad, bad. Totally suspect.
// Poor API design choice. Don't do it.
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;
}
I totally understand why this is suboptimal. The event handler of the consumer will block the producing method. It might hang or be very long-running. It might throw. There are any of a myriad of things that might be problematic with this style of API.
But here’s the operative question: What’s the alternative? What does the .NET developer community recommend as a way to surface this type of capability to the user instead of using .NET events?