C# 7
asynchronous programming
collections
coding techniques
software development

Various ways of asynchronously returning a collection with C 7 features

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Returning collections asynchronously in C# is common for I/O-driven services, repository layers, and API aggregation code. In C# 7-era codebases, most patterns rely on Task<List<T>>, Task<T[]>, and Task.WhenAll. Even without newer async streams, you can write clear and efficient async collection flows.

The main design tradeoff is eager materialization versus incremental consumption. This article focuses on C# 7-compatible approaches and where each pattern fits.

Core Sections

1. Return a materialized list with Task<List<T>>

csharp
1public async Task<List<UserDto>> GetUsersAsync()
2{
3    var rows = await _db.QueryAsync<UserRow>("select id, name from users");
4    return rows.Select(r => new UserDto(r.Id, r.Name)).ToList();
5}

This is simple and works well when caller needs full collection immediately.

2. Parallel fetch and aggregate with Task.WhenAll

csharp
1public async Task<IReadOnlyList<OrderDto>> GetOrdersAsync(IEnumerable<int> ids)
2{
3    var tasks = ids.Select(id => _client.FetchOrderAsync(id));
4    var results = await Task.WhenAll(tasks);
5    return results;
6}

Use this for independent remote calls. Add throttling if fan-out can overload downstream services.

3. Use tuples and local functions for readable pipelines

csharp
1public async Task<List<(int Id, decimal Total)>> LoadTotalsAsync(int[] ids)
2{
3    async Task<(int, decimal)> LoadOne(int id)
4    {
5        var total = await _repo.GetTotalAsync(id);
6        return (id, total);
7    }
8
9    var items = await Task.WhenAll(ids.Select(LoadOne));
10    return items.ToList();
11}

C# 7 tuples reduce ceremony and make intermediate async collection results easier to handle.

4. Cancellation and error strategy

Propagate CancellationToken through all async calls. Decide whether one failing item should fail whole operation or return partial results with error details. Document this behavior because callers depend on it.

For APIs, prefer predictable contracts: either all-or-nothing success, or explicit per-item status in response model.

5. Build repeatable verification around asynchronous collection-return patterns in C#

After implementation works once, lock in behavior with repeatable verification artifacts. At minimum, maintain one baseline case, one edge case, and one failure-path case with expected outcomes written down in plain language. This prevents accidental regressions when dependencies, runtime versions, or surrounding infrastructure change.

Use lightweight automation for these checks so they run in local development and CI. A practical pattern is to keep a tiny fixture dataset and one command that executes the critical path end to end. If that command fails, engineers can reproduce issues quickly without rebuilding the entire environment from scratch.

text
1verification checklist
2- baseline scenario with expected output
3- edge scenario with constrained input
4- failure scenario with expected error behavior
5- runtime and dependency versions captured

Treat this checklist as versioned code-adjacent documentation. Updating asynchronous collection-return patterns in C# without updating its verification contract is a common source of drift and support incidents.

6. Operational guidance and maintenance strategy

The long-term reliability of asynchronous collection-return patterns in C# depends on observability and change discipline. Add structured logging and targeted metrics around the most failure-prone stages so you can answer quickly: what input was processed, what branch was taken, and why output changed. Incident response improves dramatically when these signals exist before the outage.

Also define ownership for changes. When libraries, runtime versions, or platform policies evolve, someone should review compatibility and re-run validation artifacts before rollout. Small proactive checks are cheaper than emergency rollback windows.

Finally, schedule periodic contract checks even when no incident is active. Silent drift accumulates over time through dependency updates and environment differences. Preventive checks keep asynchronous collection-return patterns in C# predictable and reduce production surprises.

Common Pitfalls

  • Mixing synchronous .Result with async code and risking thread pool starvation.
  • Fan-out with Task.WhenAll without concurrency limits on large input sets.
  • Returning mutable collections where callers assume immutability.
  • Hiding partial failures and returning incomplete collections silently.
  • Forgetting cancellation token propagation in nested async calls.

Summary

In C# 7 code, asynchronous collection returns are best handled with Task<TCollection> patterns, careful Task.WhenAll fan-out, and explicit error/cancellation contracts. Tuples and local functions help keep pipelines readable without extra types. With these practices, async collection workflows remain efficient and maintainable even before adopting newer language features like async streams.


Course illustration
Course illustration

All Rights Reserved.