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;
}
}
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.
Specifc type is used to return the Primitive (string, boolean, int, etc) or Complex data (Custom object type) from an action method.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With