Signal Forge ADR-010: gRPC server-streaming via AsAsyncEnumerable (not ToListAsync)
Status: Accepted
Decision: GetOrdersByProject streams rows directly from
the PostgreSQL cursor using AsAsyncEnumerable(), writing each row to the gRPC stream as it is
fetched.
Code pattern:
await foreach (var order in _db.Orders
.Where(o => o.ProjectId == request.ProjectId)
.OrderByDescending(o => o.CreatedAt)
.AsAsyncEnumerable()
.WithCancellation(context.CancellationToken))
{
await responseStream.WriteAsync(MapToResponse(order), context.CancellationToken);
}
Rationale:
ToListAsync()loads all matching rows into aList<Order>in application memory before streaming begins. For large result sets this causes OOM.AsAsyncEnumerable()uses the database cursor: one row is fetched, sent over gRPC, then the next is fetched. Memory usage is O(1) regardless of result set size.- CancellationToken is threaded through so the DB query is cancelled if the gRPC client disconnects.
Alternative considered: ToListAsync() — rejected due to unbounded memory growth.