skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.

    public async static Task WriteAsJsonAsync<T>(this IAsyncEnumerable<T> content, HttpResponse response)
    {
        bool first = true;
        await foreach(var item in content)
        {
            if (first)
            {
                // Provide JSON header, same as in JsonConstants, but that's not visible to us
                response.ContentType = "application/json; charset=utf-8";
    
                // As we're dealing with an Enumerable, open an array
                await response.WriteAsync("[");
                first = false;
            }
            else
            {
                // After the first entry, we're going to want to prefix all remaining with a comma
                await response.WriteAsync(",");
            }
    
            // Use the JsonSerializer to serialise each item to the response stream
            await JsonSerializer.SerializeAsync(utf8Json: response.Body,
                                    value: item,
                                    options: null,
                                    cancellationToken: response.HttpContext.RequestAborted);
        }
    
        // Close the array
        await response.WriteAsync("]");
    }
    

    Which can be used with the example method with the following adjustments:

    public async Task<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
        {
            await GetSlowData().WriteAsJsonAsync(base.Response);
            await base.Response.CompleteAsync();
            return new EmptyResult();
        }
        catch
        {
            return BadRequest("GetSlowData had a problem with you.");
        }
    }
    

    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.


  2. As you noted, the problem is in the return type. Any async method returning an IAsyncEnumerable 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:

    public IActionResult ReturnWithHandling()
    {
      // (unchanged)
      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
      {
        IAsyncEnumerable<int> result = GetSlowData();
        IAsyncEnumerator<int> enumerator = result.GetEnumerator();
        
        if (!await enumerator.MoveNextAsync())
        {
          enumerator.Dispose();
          return Ok(new int[0]); // or whatever your empty response is
        }
    
        return Ok(enumerator.Current, enumerator);
      }
      catch
      {
        return BadRequest("GetSlowData had a problem with you.");
      }
    }
    
    private static async IAsyncEnumerable<T> FirstAndRest<T>(T first, IAsyncEnumerator<T> rest)
    {
      using var _ = rest;
      yield return first;
      while (await enumerator.MoveNextAsync())
        yield return enumerator.Current;
    }
    

    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). The FirstAndRest helper just stitches that first item onto the rest of the asynchronous sequence, so it’s the same sequence again.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search