Async Await
What is Async/Await?
Async/await is C#’s way of handling asynchronous programming - essentially allowing your code to “wait” for long-running operations without blocking the current thread. Think of it as a way to write code that looks synchronous but behaves asynchronously.
The magic happens with two keywords:
async- marks a method as asynchronousawait- tells the method to wait for an async operation to complete
Here’s the simplest example:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000); // Simulate a 1-second operation
return "Hello World!";
}
Why Do We Need This?
The Problem with Synchronous Code
Imagine your API needs to call an external service:
[HttpGet("weather")]
public IActionResult GetWeather()
{
// This BLOCKS the thread for however long the HTTP call takes
var weatherData = httpClient.GetStringAsync("https://api.weather.com/data").Result;
return Ok(weatherData);
}
This is problematic because:
- The thread is blocked waiting for the HTTP response
- Your server can only handle a limited number of concurrent requests
- Under load, you’ll quickly exhaust your thread pool
- Users experience slow response times
The Async Solution
With async/await, the thread is freed up while waiting:
[HttpGet("weather")]
public async Task<IActionResult> GetWeatherAsync()
{
// Thread is freed up while waiting for the HTTP response
var weatherData = await httpClient.GetStringAsync("https://api.weather.com/data");
return Ok(weatherData);
}
Now the thread can handle other requests while waiting for the weather API to respond!
Task and Task
In .NET, asynchronous operations return either:
Task- represents an async operation that doesn’t return a valueTask<T>- represents an async operation that returns a value of typeT
// Returns Task (no return value)
public async Task SaveDataAsync(string data)
{
await database.SaveAsync(data);
// No return statement needed
}
// Returns Task<string> (returns a string)
public async Task<string> LoadDataAsync()
{
var result = await database.LoadAsync();
return result; // Returns string, but method signature is Task<string>
}
Common Patterns in APIs
Controller Actions
API controllers benefit from async patterns:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(Guid id)
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(CreateProductRequest request)
{
var product = await _productService.CreateAsync(request);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
Service Layer
Your services should embrace async throughout:
public interface IProductService
{
Task<IEnumerable<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(Guid id);
Task<Product> CreateAsync(CreateProductRequest request);
Task UpdateAsync(Guid id, UpdateProductRequest request);
Task DeleteAsync(Guid id);
}
public class ProductService : IProductService
{
private readonly AppDbContext _context;
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
public async Task<Product?> GetByIdAsync(Guid id)
{
return await _context.Products.FindAsync(id);
}
// ... other methods
}
Entity Framework Core
EF Core is built for async from the ground up:
// Query operations
var products = await context.Products.Where(p => p.IsActive).ToListAsync();
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == id);
var exists = await context.Products.AnyAsync(p => p.Name == name);
// Save operations
context.Products.Add(newProduct);
await context.SaveChangesAsync();
HTTP Client Calls
Making external API calls is where async really shines:
public class WeatherService
{
private readonly HttpClient _httpClient;
public async Task<WeatherData> GetWeatherAsync(string city)
{
var response = await _httpClient.GetAsync($"api/weather/{city}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<WeatherData>(json);
}
public async Task<WeatherData> PostWeatherDataAsync(WeatherData data)
{
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("api/weather", content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<WeatherData>(responseJson);
}
}
Multiple Async Operations
Sequential vs Parallel
Sequential - operations run one after another:
public async Task<CombinedData> GetDataSequentiallyAsync()
{
var userData = await userService.GetUserAsync(userId); // Wait for this...
var orderData = await orderService.GetOrdersAsync(userId); // ...then this
var productData = await productService.GetProductsAsync(); // ...then this
return new CombinedData(userData, orderData, productData);
}
Parallel - operations can run simultaneously:
public async Task<CombinedData> GetDataInParallelAsync()
{
// Start all operations at the same time
var userTask = userService.GetUserAsync(userId);
var orderTask = orderService.GetOrdersAsync(userId);
var productTask = productService.GetProductsAsync();
// Wait for all to complete
await Task.WhenAll(userTask, orderTask, productTask);
// Get the results
var userData = await userTask;
var orderData = await orderTask;
var productData = await productTask;
return new CombinedData(userData, orderData, productData);
}
The parallel version will be significantly faster if the operations don’t depend on each other!
Common Pitfalls
1. Don’t Block on Async Code
Never do this:
// DON'T DO THIS - can cause deadlocks
var result = SomeAsyncMethod().Result;
var result2 = SomeAsyncMethod().GetAwaiter().GetResult();
Do this instead:
// DO THIS
var result = await SomeAsyncMethod();
2. Async All the Way Down
If you’re using async in one place, it tends to “bubble up” through your call stack:
// If your data access is async...
public async Task<Product> GetProductFromDbAsync(Guid id)
{
return await context.Products.FindAsync(id);
}
// ...your service should be async...
public async Task<Product> GetProductAsync(Guid id)
{
return await repository.GetProductFromDbAsync(id);
}
// ...and your controller should be async
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(Guid id)
{
var product = await productService.GetProductAsync(id);
return Ok(product);
}
3. ConfigureAwait Considerations
In library code, consider using ConfigureAwait(false):
public async Task<string> LibraryMethodAsync()
{
// In library code, often you don't need the original context
var result = await httpClient.GetStringAsync(url).ConfigureAwait(false);
return result.ToUpper();
}
In ASP.NET Core applications, this is less of a concern as there’s no synchronization context to worry about.
Exception Handling
Async methods handle exceptions naturally with try/catch:
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(Guid id)
{
try
{
var product = await productService.GetProductAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
catch (DatabaseException ex)
{
logger.LogError(ex, "Database error retrieving product {ProductId}", id);
return StatusCode(500, "Internal server error");
}
}
Performance Benefits
Async/await doesn’t make individual operations faster - it makes your application more scalable by:
- Better thread utilization - threads aren’t blocked waiting
- Higher concurrency - more requests can be handled simultaneously
- Improved responsiveness - UI threads stay responsive (less relevant for APIs)
- Resource efficiency - fewer threads needed overall
When NOT to Use Async
Async isn’t always the answer:
- CPU-bound operations - use
Task.Run()or consider parallel processing instead - Very fast operations - async has overhead, don’t async operations that complete in microseconds
- Simple CRUD with no external calls - though EF Core encourages async anyway
Wrap Up
Async/await is fundamental to modern .NET development, especially in web APIs where you’re constantly dealing with databases, external services, and I/O operations.
The key mental shift is understanding that await doesn’t block threads - it frees them up to handle other work while waiting for operations to complete. This leads to more scalable, responsive applications that can handle higher loads with fewer resources.
Start with making your controller actions async, embrace async throughout your service layer, and use Entity Framework’s async methods. Once you get the hang of it, you’ll wonder how you ever built APIs without it!