async
await
task
web api
.NET Core

Using async/await or task in web api controller .net core

Master System Design with Codemia

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

Introduction

ASP.NET Core Web API controllers should use async/await for all I/O-bound operations (database queries, HTTP calls, file reads) to keep threads free for handling other requests. An async controller action returns Task<IActionResult> or Task<T> and uses await on asynchronous methods. This does not make the operation faster for a single request but allows the server to handle more concurrent requests because threads are not blocked waiting for I/O. Synchronous controller actions block the thread pool and limit scalability under load.

Basic Async Controller

csharp
1using Microsoft.AspNetCore.Mvc;
2
3[ApiController]
4[Route("api/[controller]")]
5public class ProductsController : ControllerBase
6{
7    private readonly IProductRepository _repository;
8
9    public ProductsController(IProductRepository repository)
10    {
11        _repository = repository;
12    }
13
14    // Async action — thread released during database call
15    [HttpGet]
16    public async Task<IActionResult> GetAll()
17    {
18        var products = await _repository.GetAllAsync();
19        return Ok(products);
20    }
21
22    [HttpGet("{id}")]
23    public async Task<ActionResult<Product>> GetById(int id)
24    {
25        var product = await _repository.GetByIdAsync(id);
26        if (product == null)
27            return NotFound();
28        return product;
29    }
30
31    [HttpPost]
32    public async Task<IActionResult> Create([FromBody] ProductDto dto)
33    {
34        var product = await _repository.CreateAsync(dto);
35        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
36    }
37}

Each await releases the thread back to the thread pool. While the database query runs, the thread handles other incoming requests.

Synchronous vs Async Comparison

csharp
1// WRONG — synchronous blocks the thread
2[HttpGet("sync")]
3public IActionResult GetProductsSync()
4{
5    // Thread is BLOCKED during database call
6    var products = _repository.GetAll();  // Synchronous
7    return Ok(products);
8}
9
10// CORRECT — async releases the thread
11[HttpGet("async")]
12public async Task<IActionResult> GetProductsAsync()
13{
14    // Thread is RELEASED during database call
15    var products = await _repository.GetAllAsync();
16    return Ok(products);
17}

Under load with 100 concurrent requests, the synchronous version blocks 100 threads. The async version needs far fewer threads because each is released during I/O waits.

Multiple Async Operations

csharp
1[HttpGet("dashboard")]
2public async Task<IActionResult> GetDashboard()
3{
4    // WRONG — sequential, each waits for the previous
5    // var users = await _userRepo.GetCountAsync();
6    // var orders = await _orderRepo.GetRecentAsync();
7    // var revenue = await _statsRepo.GetRevenueAsync();
8
9    // CORRECT — concurrent, all run at the same time
10    var usersTask = _userRepo.GetCountAsync();
11    var ordersTask = _orderRepo.GetRecentAsync();
12    var revenueTask = _statsRepo.GetRevenueAsync();
13
14    await Task.WhenAll(usersTask, ordersTask, revenueTask);
15
16    return Ok(new
17    {
18        UserCount = usersTask.Result,
19        RecentOrders = ordersTask.Result,
20        Revenue = revenueTask.Result,
21    });
22}

Start all independent tasks before awaiting any of them. Task.WhenAll completes when all three finish, cutting total time from sum to max.

Calling External APIs

csharp
1[ApiController]
2[Route("api/[controller]")]
3public class WeatherController : ControllerBase
4{
5    private readonly IHttpClientFactory _httpClientFactory;
6
7    public WeatherController(IHttpClientFactory httpClientFactory)
8    {
9        _httpClientFactory = httpClientFactory;
10    }
11
12    [HttpGet("{city}")]
13    public async Task<IActionResult> GetWeather(string city)
14    {
15        var client = _httpClientFactory.CreateClient("weather");
16
17        var response = await client.GetAsync($"/api/weather?q={city}");
18        if (!response.IsSuccessStatusCode)
19            return StatusCode((int)response.StatusCode);
20
21        var content = await response.Content.ReadAsStringAsync();
22        return Ok(content);
23    }
24}
25
26// Registration in Program.cs
27builder.Services.AddHttpClient("weather", client =>
28{
29    client.BaseAddress = new Uri("https://api.weather.com");
30    client.Timeout = TimeSpan.FromSeconds(10);
31});

Always use IHttpClientFactory — creating HttpClient directly causes socket exhaustion.

Async with Entity Framework Core

csharp
1public class ProductRepository : IProductRepository
2{
3    private readonly AppDbContext _context;
4
5    public ProductRepository(AppDbContext context)
6    {
7        _context = context;
8    }
9
10    public async Task<List<Product>> GetAllAsync()
11    {
12        return await _context.Products.ToListAsync();
13    }
14
15    public async Task<Product?> GetByIdAsync(int id)
16    {
17        return await _context.Products.FindAsync(id);
18    }
19
20    public async Task<Product> CreateAsync(ProductDto dto)
21    {
22        var product = new Product { Name = dto.Name, Price = dto.Price };
23        _context.Products.Add(product);
24        await _context.SaveChangesAsync();
25        return product;
26    }
27}

EF Core provides async versions of all database operations: ToListAsync(), FindAsync(), SaveChangesAsync(), FirstOrDefaultAsync(), etc.

Cancellation Tokens

csharp
1[HttpGet]
2public async Task<IActionResult> GetProducts(CancellationToken cancellationToken)
3{
4    // If the client disconnects, the token is cancelled
5    var products = await _context.Products
6        .ToListAsync(cancellationToken);
7
8    return Ok(products);
9}
10
11// Pass the token through to all async calls
12[HttpGet("search")]
13public async Task<IActionResult> Search(string query, CancellationToken ct)
14{
15    var dbResults = await _repository.SearchAsync(query, ct);
16    var apiResults = await _externalApi.SearchAsync(query, ct);
17    return Ok(new { dbResults, apiResults });
18}

ASP.NET Core passes a CancellationToken that triggers when the client disconnects. Passing it to async operations cancels unnecessary work.

Common Pitfalls

  • Using .Result or .Wait() in controllers: task.Result and task.Wait() block the thread synchronously, defeating the purpose of async and risking deadlocks. Always use await.
  • Async void controller actions: async void methods cannot be awaited, and unhandled exceptions crash the process. Controller actions must return Task<IActionResult>, not void.
  • Synchronous wrappers around async code: Task.Run(() => syncMethod()) offloads to a thread pool thread but does not free it — the thread is still blocked. Only use async for naturally async operations (I/O).
  • Not using CancellationToken: Without cancellation tokens, long-running database queries continue executing even after the client disconnects. Pass CancellationToken to all async methods.
  • Creating HttpClient directly: new HttpClient() inside controller actions causes socket exhaustion under load. Use IHttpClientFactory for proper connection pooling and lifetime management.

Summary

  • Use async Task<IActionResult> for all controller actions with I/O operations
  • await releases the thread during I/O, allowing the server to handle more concurrent requests
  • Use Task.WhenAll for independent operations instead of sequential await
  • Always pass CancellationToken to async methods to cancel work when clients disconnect
  • Use IHttpClientFactory for HTTP calls, never new HttpClient() directly
  • Never use .Result, .Wait(), or Task.Run() with synchronous code in ASP.NET Core controllers

Course illustration
Course illustration

All Rights Reserved.