Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning IAsyncEnumerable<T> and NotFound from Asp.Net Core Controller

What is the right signature for a controller action that returns an IAsyncEnumerable<T> and a NotFoundResult but is still processed in an async fashion?

I used this signature and it doesn't compile because IAsyncEnumerable<T> isn't awaitable:

[HttpGet]
public async Task<IActionResult> GetAll(Guid id)
{
    try
    {
        return Ok(await repository.GetAll(id)); // GetAll() returns an IAsyncEnumerable
    }
    catch (NotFoundException e)
    {
        return NotFound(e.Message);
    }
}

This one compiles fine but its signature isn't async. So I'm worried whether it'll block thread pool threads or not:

[HttpGet]
public IActionResult GetAll(Guid id)
{
    try
    {
        return Ok(repository.GetAll(id)); // GetAll() returns an IAsyncEnumerable
    }
    catch (NotFoundException e)
    {
        return NotFound(e.Message);
    }
}

I tried using a await foreach loop on like this but that obviously wouldn't compile either:

[HttpGet]
public async IAsyncEnumerable<MyObject> GetAll(Guid id)
{
    IAsyncEnumerable<MyObject> objects;
    try
    {
        objects = repository.GetAll(id); // GetAll() returns an IAsyncEnumerable
    }
    catch (NotFoundException e)
    {
        return NotFound(e.Message);
    }

    await foreach (var obj in objects)
    {
        yield return obj;
    }
}
like image 942
Frederick The Fool Avatar asked Jan 21 '20 12:01

Frederick The Fool


People also ask

What are the return types in .NET Core?

Some common return types in this category are BadRequestResult (400), NotFoundResult (404), and OkObjectResult (200). Alternatively, convenience methods in the ControllerBase class can be used to return ActionResult types from an action.

What is return type in asp net?

Specifc type is used to return the Primitive (string, boolean, int, etc) or Complex data (Custom object type) from an action method.


1 Answers

Option 2, which passes an implementation of IAsyncEnumerable<> into the Ok call, is fine. The ASP.NET Core plumbing takes care of the enumeration and is IAsyncEnumerable<>-aware as of 3.0.

Here's the call from the question, repeated for context:

return Ok(repository.GetAll(id)); // GetAll() returns an IAsyncEnumerable

The call to Ok creates an instance of OkObjectResult, which inherits ObjectResult. The value passed in to Ok is of type object, which is held in the ObjectResult's Value property. ASP.NET Core MVC uses the command pattern, whereby the command is an implementation of IActionResult and is executed using an implementation of IActionResultExecutor<T>.

For ObjectResult, ObjectResultExecutor is used to turn the ObjectResult into a HTTP response. It's the implementation of ObjectResultExecutor.ExecuteAsync that is IAsyncEnumerable<>-aware:

public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
    // ...

    var value = result.Value;

    if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
    {
        return ExecuteAsyncEnumerable(context, result, value, reader);
    }

    return ExecuteAsyncCore(context, result, objectType, value);
}

As the code shows, the Value property is checked to see if it implements IAsyncEnumerable<> (the details are hidden in the call to TryGetReader). If it does, ExecuteAsyncEnumerable is called, which performs the enumeration and then passes the enumerated result into ExecuteAsyncCore:

private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, object asyncEnumerable, Func<object, Task<ICollection>> reader)
{
    Log.BufferingAsyncEnumerable(Logger, asyncEnumerable);

    var enumerated = await reader(asyncEnumerable);
    await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated);
}

reader in the above snippet is where the enumeration occurs. It's buried a little, but you can see the source here:

private async Task<ICollection> ReadInternal<T>(object value)
{
    var asyncEnumerable = (IAsyncEnumerable<T>)value;
    var result = new List<T>();
    var count = 0;

    await foreach (var item in asyncEnumerable)
    {
        if (count++ >= _mvcOptions.MaxIAsyncEnumerableBufferLimit)
        {
            throw new InvalidOperationException(Resources.FormatObjectResultExecutor_MaxEnumerationExceeded(
                nameof(AsyncEnumerableReader),
                value.GetType()));
        }

        result.Add(item);
    }

    return result;
}

The IAsyncEnumerable<> is enumerated into a List<> using await foreach, which, almost by definition, doesn't block a request thread. As Panagiotis Kanavos called out in a comment on the OP, this enumeration is performed in full before a response is sent back to the client.

like image 96
Kirk Larkin Avatar answered Sep 25 '22 14:09

Kirk Larkin