C#  

Asynchronous Programming (Async / Await) in C# and .NET Core

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:

  • Task and Task<T>

  • async and await keywords

  • Asynchronous I/O (database calls, HTTP calls, file I/O)

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:

  1. 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.

  2. 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.

  3. 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.

  • Task → represents a method that returns no result (similar to void).

  • Task<T> → represents a method that returns a result of type T.

Examples:

public Task DoWorkAsync()
{
    // Some asynchronous work
}

public Task<string> GetNameAsync()
{
    // Returns string asynchronously
}

The async and await Keywords

async

  • async is used in method signatures.

  • It tells the compiler: “This method may contain await and will execute asynchronously.”

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

  • File I/O (read/write file)

  • Database queries

  • HTTP calls (Web API, external service)

  • Sending emails, etc.

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

  • Complex calculations

  • Image processing

  • Data processing in memory

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

  1. Blocking with .Result or .Wait()

var result = GetDataAsync().Result;  // Bad in ASP.NET or UI

This can cause deadlocks or thread starvation.

  1. Using async void for non-event methods

    • Always prefer Task or Task<T>

    • async void is only for event handlers (e.g., button click in UI).

  2. Forgetting to add async all the way up

    • If a repository is async, the controller should also be async.

    • Do not call async code synchronously.

  3. Mixing synchronous and asynchronous I/O

    • Use async all the way: ToListAsync, ReadAsync, GetAsync.

Best Practices for Asynchronous Programming

  1. Use async and await for I/O operations.

  2. Avoid .Wait() and .Result in ASP.NET and UI apps.

  3. Return Task or Task<T> from async methods, not void.

  4. Use async everywhere in the call chain (from controller to repository).

  5. Name async methods with the suffix Async (e.g., GetUserAsync).

  6. Be careful with shared data, even in async methods; concurrency issues can still occur.

  7. 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

FeatureAsync/AwaitTask.RunThreads
Creates a new thread?NoYes (ThreadPool)Yes
Ideal forI/O-boundCPU-boundCPU-bound or long tasks
Blocks thread?NoA thread is used for workYes
Best for Web API?YesNoNo
Used for Parallel work?NoYes (sometimes)Yes
ComplexityVery easyMediumHigh
Resource usageVery lowMediumHigh