Modern applications are expected to be fast, responsive, and scalable. Users do not want to wait, and servers must handle many requests at the same time. This is where asynchronous programming becomes very important.
In C#, asynchronous programming is mainly done using:
This article explains:
What synchronous vs asynchronous means
Why async programming is needed
Task, async, await basics
I/O-bound vs CPU-bound operations
How async works in ASP.NET Core
Common mistakes
Best practices
A full practical example (Web API + async DB call style)
Synchronous vs Asynchronous – Simple Explanation
Synchronous Code
In synchronous code:
Each line waits for the previous line to finish.
If a slow operation (like a database call) takes 5 seconds, the whole thread is blocked for 5 seconds.
The application becomes unresponsive if many such operations happen at the same time.
Example (synchronous):
public string GetData()
{
// Simulate long running task
Thread.Sleep(5000);
return "Data loaded";
}
If this runs on the UI thread or in an API, everything waits.
Asynchronous Code
In asynchronous code:
The thread starts an operation (like a DB or HTTP call) and does not wait by blocking.
While the operation is in progress, the thread is free to serve other requests.
When the result is ready, the continuation resumes.
Example (asynchronous):
public async Task<string> GetDataAsync()
{
await Task.Delay(5000);
return "Data loaded";
}
Here, the thread is not blocked for 5 seconds; it can do other work.
Why Do We Need Asynchronous Programming?
Asynchronous programming is mainly about:
Scalability in Web Applications
In ASP.NET Core, each incoming request uses a thread.
If threads are blocked by I/O operations (database, API calls), the server can handle fewer requests.
Async frees threads to handle more requests, improving performance under load.
Responsiveness in UI Apps
In desktop or mobile apps, blocking the UI thread makes the app freeze.
Async keeps the UI responsive while work is happening in the background.
Efficient Resource Usage
Async uses threads more efficiently.
Better for cloud, microservices, and high-traffic systems.
Understanding Task and Task<T>
In C#, Task represents an asynchronous operation.
Examples:
public Task DoWorkAsync()
{
// Some asynchronous work
}
public Task<string> GetNameAsync()
{
// Returns string asynchronously
}
The async and await Keywords
async
Example:
public async Task<string> GetMessageAsync()
{
// method body
}
await
await tells the method to asynchronously wait for a Task to complete.
It does not block the thread.
After the awaited task completes, the method continues.
Example:
public async Task<string> GetMessageAsync()
{
await Task.Delay(2000); // simulate delay
return "Hello after 2 seconds";
}
I/O-Bound vs CPU-Bound Operations
Understanding this helps you choose the right async pattern.
I/O-Bound Operations
Use: async/await with I/O methods like ReadAsync, ExecuteReaderAsync, GetAsync.
Example:
public async Task<string> GetFromApiAsync()
{
using var client = new HttpClient();
var response = await client.GetStringAsync("https://example.com");
return response;
}
CPU-Bound Operations
For CPU-bound work, you can use:
await Task.Run(() => DoHeavyWork());
But this still uses a thread; it just moves the work off the main thread.
Async in ASP.NET Core – How It Works
In ASP.NET Core:
Each HTTP request is handled by a thread from the thread pool.
When you use await for I/O (like DB or HTTP), the thread is returned to the pool while it waits.
This lets the server handle more requests with fewer threads.
Example (Controller):
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repo;
public ProductsController(IProductRepository repo)
{
_repo = repo;
}
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _repo.GetAllAsync();
return Ok(products);
}
}
Practical End-to-End Example (Repository + Service + Controller)
Imagine a simple Product API using async.
Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
Repository Interface
public interface IProductRepository
{
Task<List<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task AddAsync(Product product);
}
Repository Implementation (using Entity Framework Core style)
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
public async Task<Product?> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
public async Task AddAsync(Product product)
{
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
}
}
Notice:
ToListAsync, FindAsync, AddAsync, SaveChangesAsync – all asynchronous.
No thread blocking while waiting for the database.
Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repo;
public ProductsController(IProductRepository repo)
{
_repo = repo;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var products = await _repo.GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _repo.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<IActionResult> Post(Product product)
{
await _repo.AddAsync(product);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
}
This is a typical asynchronous Web API pattern in ASP.NET Core.
Common Mistakes in Async Programming
Blocking with .Result or .Wait()
var result = GetDataAsync().Result; // Bad in ASP.NET or UI
This can cause deadlocks or thread starvation.
Using async void for non-event methods
Forgetting to add async all the way up
Mixing synchronous and asynchronous I/O
Best Practices for Asynchronous Programming
Use async and await for I/O operations.
Avoid .Wait() and .Result in ASP.NET and UI apps.
Return Task or Task<T> from async methods, not void.
Use async everywhere in the call chain (from controller to repository).
Name async methods with the suffix Async (e.g., GetUserAsync).
Be careful with shared data, even in async methods; concurrency issues can still occur.
Log and handle exceptions in async methods using try-catch around awaited calls.
Simple Example Showing Async Benefit
Imagine a controller calling a slow external API.
Synchronous version:
[HttpGet("sync")]
public IActionResult GetDataSync()
{
var client = new HttpClient();
var response = client.GetStringAsync("https://example.com").Result; // blocks thread
return Ok(response);
}
Asynchronous version:
[HttpGet("async")]
public async Task<IActionResult> GetDataAsync()
{
var client = new HttpClient();
var response = await client.GetStringAsync("https://example.com"); // non-blocking
return Ok(response);
}
In high-load scenarios, the async version allows the server to use its threads much more efficiently.
Async vs Thread vs Task.Run – Side-by-Side Comparison
| Feature | Async/Await | Task.Run | Threads |
|---|
| Creates a new thread? | No | Yes (ThreadPool) | Yes |
| Ideal for | I/O-bound | CPU-bound | CPU-bound or long tasks |
| Blocks thread? | No | A thread is used for work | Yes |
| Best for Web API? | Yes | No | No |
| Used for Parallel work? | No | Yes (sometimes) | Yes |
| Complexity | Very easy | Medium | High |
| Resource usage | Very low | Medium | High |