Async Basics in .NET
2.1 The Task-Based Asynchronous Pattern (TAP)
.NET follows the Task-based Asynchronous Pattern (TAP) for implementing asynchronous operations.
In TAP:
- Every async operation returns a Task or Task<T>
- Task represents an operation in progress
- Task<T> represents an operation that returns a value
Why TAP Exists
Earlier .NET versions used:
- Callbacks
- Events
- Thread manually management
These approaches were:
- Hard to read
- Hard to debug
- Error-prone
TAP simplifies this by letting you write async code almost like synchronous code.
2.2 Understanding Task, Task<T>, and ValueTask
1. Task
Used when the async method does not return any value.
public async Task LogActivityAsync(string message)
{
await _logger.WriteAsync(message);
}
2. Task<T>
Used when the async method returns a value.
public async Task<User> GetUserByIdAsync(int id)
{
return await _userRepository.GetByIdAsync(id);
}
3. ValueTask
Used in high-performance scenarios to reduce heap allocations when results are often available synchronously.
public async ValueTask<int> GetCachedCountAsync()
{
if (_cache.TryGetValue("count", out int count))
return count; // No heap allocation
count = await _repository.GetCountAsync();
_cache.Set("count", count);
return count;
}
⚠️ Use ValueTask only when performance profiling proves it is needed.
In normal APIs, Task and Task<T> are sufficient.
2.3 The async and await Keywords (Deep Dive)
What async Does
- Marks a method as asynchronous
- Allows the use of await inside the method
- Automatically wraps return values in Task or Task<T>
What await Does
- Pauses the method without blocking the thread
- Releases the thread back to the ThreadPool
- Resumes execution when the awaited task completes
Basic Example
public async Task<string> GetCustomerNameAsync(int id)
{
var customer = await _db.Customers.FindAsync(id);
return customer.Name;
}
Execution flow:
- Request thread starts execution
- Database call begins
- Thread is released
- Database completes
- Method resumes
- Result is returned
2.4 Async vs Sync: Real Database Example
Synchronous Version (Bad for APIs)
public List<Product> GetProducts()
{
return _db.Products.ToList(); // Blocks thread
}
Asynchronous Version (Recommended)
public async Task<List<Product>> GetProductsAsync()
{
return await _db.Products.ToListAsync(); // Non-blocking
}
With the async version:
- Request thread is not blocked
- Server can handle more clients
- Better performance under load
2.5 CPU-Bound vs I/O-Bound Operations
Understanding this difference is critical for correct async usage.
I/O-Bound Operations (Use Async ✅)
These wait on external systems:
- Database calls
- HTTP calls
- File read/write
- Message brokers
public async Task<FileData> ReadFileAsync(string path)
{
var bytes = await File.ReadAllBytesAsync(path);
return new FileData(bytes);
}
CPU-Bound Operations (Use Parallelism, Not Async ⚠️)
These consume CPU cycles:
- Complex calculations
- Image processing
- Encryption
public int CalculateHash(byte[] data)
{
return _hashService.GenerateHash(data); // CPU-bound
}
If you wrap CPU-bound work in Task.Run() inside APIs, you may:
- Overuse ThreadPool threads
- Reduce overall throughput
2.6 What Happens Internally with await (Simplified)
When the compiler sees:
await SomeAsyncMethod();
It transforms your method into a state machine that:
- Saves the current execution state
- Returns control to the caller
- Registers a continuation
- Resumes execution when the awaited task completes
You get:
- Non-blocking behavior
- Clean code
- No manual thread handling
2.7 Common Beginner Mistakes
❌ 1. Blocking on Async
var user = _userService.GetUserAsync(id).Result; // Dangerous
This can cause:
- Deadlocks
- Thread starvation
✅ Correct:
var user = await _userService.GetUserAsync(id);
❌ 2. Mixing Sync and Async Randomly
public async Task<IActionResult> Get()
{
var data = _service.GetData(); // Sync call
return Ok(data);
}
✅ Correct:
public async Task<IActionResult> Get()
{
var data = await _service.GetDataAsync();
return Ok(data);
}
❌ 3. Using async void in APIs (Never Do This)
public async void Process()
{
await _service.DoWorkAsync(); // Exceptions cannot be caught!
}
✅ Only use:
- async Task
- async Task<T>
2.8 Real-Life Example: Async Service with Repository
Repository
public interface IProductRepository
{
Task<List<Product>> GetAllAsync();
}
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _db;
public ProductRepository(AppDbContext db)
{
_db = db;
}
public async Task<List<Product>> GetAllAsync()
{
return await _db.Products.ToListAsync();
}
}
Service
public class ProductService : IProductService
{
private readonly IProductRepository _repo;
public ProductService(IProductRepository repo)
{
_repo = repo;
}
public async Task<List<Product>> GetProductsAsync()
{
return await _repo.GetAllAsync();
}
}
Controller
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _service.GetProductsAsync();
return Ok(products);
}
✅ The entire request flow is now:
Controller → Service → Repository → Database
Fully asynchronous and non-blocking.
Chapter 2 Summary
- TAP is the standard async model in .NET
- Task = no return value
- Task<T> = returns a value
- ValueTask = advanced performance optimization
- async / await make non-blocking code readable
- Use async for:
- I/O-bound operations
- Avoid:
- .Result, .Wait()
- async void
- Sync-over-async