With ASP.NET Web API, we generally use IActionResult
as a return type for a method, potentially wrapped in a Task<>
when working asynchronously.
We now have IAsyncEnumerable<>
return type to support yield return
statements and provide an unbuffered result for streaming a I/O intensive result set back.
Conventionally we use an IActionResult
return instead of a specific-type when we want to provide some exception handling, e.g:
public IActionResult DoSomething(string? data)
{
try
{
// We're making stuff up for an example
if (data == null)
throw new ArgumentNullException(nameof(data));
// Do something else
var myResults = SomeProcessing();
return Ok(myResults);
}
catch
{
return BadRequest("Your request was not understood, etc.");
}
}
But we have to use IAsyncEnumerable<>
if we want to start returning immediately:
public async IAsyncEnumerable<int> GetSlowData()
{
var sample = Enumerable.Range(0, 10);
foreach(var entry in sample)
{
await Task.Delay(1000);
yield return entry;
}
}
Where I’m struggling is, how to mix both of these concepts with error handling. I want to be able to return a BadRequest, but also return data in an unbuffered way through IAsyncEnumerable
.
public IActionResult ReturnWithHandling()
{
async IAsyncEnumerable<int> GetSlowData()
{
var sample = Enumerable.Range(0, 10);
throw new Exception("Oh no! Sabotage!");
foreach (var entry in sample)
{
await Task.Delay(1000);
yield return entry;
}
}
try
{
return Ok(GetSlowData());
}
catch
{
return BadRequest("GetSlowData had a problem with you.");
}
}
This cracks the main problem in that it will compile and return an unbuffered result as data is available. But if there is an exception caused in our IAsyncEnumerable
method, it is not caught by the method; an error 500 is returned instead of a Bad Request. This is because wrapping up the call in an Ok()
action means that the action isn’t executed within the method but as part of the magic of ASP.NET.
Is there a pattern or approach to resolve this disconnect?
2
Answers
This approach writes the responses directly, which then means the method can catch any exceptions. I'm sure there must be a more elegant approach that uses middleware, but that's not something I've spent any time nosing about.
Which can be used with the example method with the following adjustments:
I added a call to
CompleteAsync()
as good measure.This makes the assumption that simply wrapping the results with a JSON array [], and adding commas between each value is sufficient.
And of course, once the headers have been written, there is no way to change to a 400 error half way through the response. But for the scenario where you could have an application exception stopping the return of any results and want to handle that more graciously than an abrupt 500 response, this ticks that.
As you noted, the problem is in the return type. Any
async
method returning anIAsyncEnumerable
is going to delay evaluation until after the response headers are sent.If you want to extract the first item immediately, then you’ll need to do that before returning the
IAsyncEnumerable
. Something like this may work:The code above extracts the first value of the asynchronous enumerator before
Ok
is returned. This won’t catch any exceptions from later asynchronous enumeration, but it will catch any exceptions from the first item (e.g., invalid downstream parameters). TheFirstAndRest
helper just stitches that first item onto the rest of the asynchronous sequence, so it’s the same sequence again.