Back to: ASP.NET Core Web API Tutorials
Asynchronous Programming in ASP.NET Core Minimal API
In this article, I will discuss how to Implement Asynchronous Programming in ASP.NET Core Minimal API with examples. Please read our previous articles discussing how to Implement Error Handling and Logging in ASP.NET Core Minimal API with Examples. We will be working with the same project that we worked on so far with ASP.NET Core Minimal API. Asynchronous programming is crucial for building efficient, responsive, and scalable web APIs. ASP.NET Core Minimal APIs offer an elegant way to implement asynchronous operations with ease.
What is Asynchronous Programming?
Asynchronous programming is a programming technique that allows our application to initiate an operation and then continue executing other work without waiting for that operation to finish. This is particularly useful when dealing with tasks that take time to complete, such as reading or writing to a disk, calling a web service over the network, or querying a database.
How it works:
- In traditional synchronous programming, when we call a method that performs a time-consuming operation (e.g., reading a large file), the calling thread is blocked and waits idly until the operation completes.
- In asynchronous programming, we initiate the operation, and the calling thread is freed to perform other tasks immediately. Once the operation completes, a notification or callback signals the program to continue processing the result.
In C#, asynchronous programming is implemented using:
- The async keyword is used to indicate that a method is asynchronous.
- The Task or Task<T> return types represent ongoing operations.
- The await keyword pauses execution of the method until the asynchronous operation completes, without blocking the thread.
This allows us to write code that looks sequential but executes without blocking threads, thus improving efficiency and responsiveness.
Why Do We Need Asynchronous Programming in Web APIs?
Web APIs typically serve multiple clients simultaneously, each sending HTTP requests that the server must handle concurrently. Handling many requests efficiently is crucial for performance and scalability.
Key challenges with synchronous programming in web servers:
- Limited Threads: Web servers maintain a thread pool to handle incoming requests. Each request typically requires a thread for processing.
- Thread Blocking: In synchronous code, when the API needs to perform a long-running task, such as querying a database or calling another web service, the thread waits (is blocked) until the operation completes.
- Thread Exhaustion: If many requests come in simultaneously and all threads are blocked waiting on I/O, the server quickly runs out of threads. New requests must wait until threads free up, leading to slow response times or timeouts.
How asynchronous programming solves these problems:
- Improved Scalability: By freeing threads while waiting for I/O (database, network, file system), asynchronous programming allows the server to use threads more efficiently. The freed threads can immediately start processing new incoming requests instead of sitting idle.
- Better Resource Utilization: Threads are limited and a relatively expensive resource. Async programming enables a web server to handle thousands of concurrent connections efficiently, without allocating a thread for every request that waits on I/O.
- Performance Gains: As threads spend less time idle, requests are handled faster, reducing waiting times and improving overall throughput. This results in more requests being processed per second with less hardware.
Real-world API scenarios are ideal for async:
- Database Operations: Fetching or updating data often involves network I/O and can take significant time.
- Calling External Services: Many APIs rely on third-party services, which can result in slow or unpredictable calls.
- File I/O: Reading from or writing to large files or cloud storage.
- Network Operations: Processing requests that involve waiting for remote responses.
Implement Asynchronous Programming in ASP.NET Core Minimal API
To implement asynchronous programming in our ASP.NET Core Minimal API, we need to refactor our repository and API endpoints to use asynchronous methods. This enables our API to handle requests without blocking threads while waiting for I/O operations, such as database calls or external service calls.Â
Modify the Repository Interface and Implementation
Update all repository methods that involve I/O or data access to their asynchronous counterparts, which return Task or Task<T>.
Modify the IEmployeeRepository interface:
Our repository interface defines the contract for accessing our data. To make it async, we need to update all methods to return Task or Task<T> types, indicating asynchronous operations. So, please modify the IEmployeeRepository interface as follows.
namespace MinimalAPIDemo.Models { public interface IEmployeeRepository { Task<List<Employee>> GetAllEmployeesAsync(); Task<Employee?> GetEmployeeByIdAsync(int id); Task<Employee> AddEmployeeAsync(Employee employee); Task<Employee?> UpdateEmployeeAsync(int id, Employee updatedEmployee); Task<bool> DeleteEmployeeAsync(int id); } }
Note: Using Task<T> informs the compiler and callers that these methods are asynchronous and will complete at a later time. The method names are suffixed with Async by convention to indicate their asynchronous nature.
Modify the EmployeeRepository Class:
In the repository class (EmployeeRepository), we need to implement these asynchronous methods. Since we are currently using an in-memory list (which is synchronous by nature), we can simulate asynchronous behavior using Task.Delay() to mimic a database or I/O delay. In real scenarios, you would use actual async calls like EF Core’s ToListAsync(), FindAsync(), etc. So, please modify the EmployeeRepository class as follows.
namespace MinimalAPIDemo.Models { public class EmployeeRepository : IEmployeeRepository { private readonly List<Employee> _employeeList = new List<Employee> { new Employee { Id = 1, Name = "John Doe", Position = "Software Engineer", Salary = 60000 }, new Employee { Id = 2, Name = "Jane Smith", Position = "Project Manager", Salary = 80000 } }; public async Task<List<Employee>> GetAllEmployeesAsync() { //Simulate database call await Task.Delay(TimeSpan.FromSeconds(1)); return _employeeList; } public async Task<Employee?> GetEmployeeByIdAsync(int id) { //Simulate database call await Task.Delay(TimeSpan.FromSeconds(1)); var employee = _employeeList.FirstOrDefault(e => e.Id == id); return employee; } public async Task<Employee> AddEmployeeAsync(Employee newEmployee) { //Simulate database call await Task.Delay(TimeSpan.FromSeconds(1)); newEmployee.Id = _employeeList.Count > 0 ? _employeeList.Max(e => e.Id) + 1 : 1; _employeeList.Add(newEmployee); return newEmployee; } public async Task<Employee?> UpdateEmployeeAsync(int id, Employee updatedEmployee) { //Simulate database call await Task.Delay(TimeSpan.FromSeconds(1)); var employee = _employeeList.FirstOrDefault(e => e.Id == id); if (employee == null) return null; employee.Name = updatedEmployee.Name; employee.Position = updatedEmployee.Position; employee.Salary = updatedEmployee.Salary; return employee; } public async Task<bool> DeleteEmployeeAsync(int id) { //Simulate database call await Task.Delay(TimeSpan.FromSeconds(1)); var employee = _employeeList.FirstOrDefault(e => e.Id == id); if (employee == null) return false; _employeeList.Remove(employee); return true; } } }
Update the Minimal API Endpoints to Async
In our Program.cs, we need to update our API endpoints to:
- Mark the handler lambdas as async.
- Use await to call the async repository methods.
- Return results after awaiting async calls.
So, please modify Program.cs as follows.
using MinimalAPIDemo.Models; namespace MinimalAPIDemo { public class Program { public static void Main(string[] args) { // Create the WebApplication builder which prepares the app with default configs var builder = WebApplication.CreateBuilder(args); // Configure logging providers builder.Logging.ClearProviders(); // Remove any default logging providers builder.Logging.AddConsole(); // Add console logger (shows logs in terminal) builder.Logging.AddDebug(); // Add debug logger (for IDE/debugging tools) // Configure JSON serialization options for HTTP responses builder.Services.ConfigureHttpJsonOptions(options => { // Disable camelCase conversion; keep property names as declared (PascalCase) options.SerializerOptions.PropertyNamingPolicy = null; }); // Add Swagger/OpenAPI support services for API documentation and UI generation builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Register the EmployeeRepository as a singleton service in DI container // This means one instance will be shared across the app lifetime builder.Services.AddSingleton<IEmployeeRepository, EmployeeRepository>(); // Build the app (finalize service registration and middleware pipeline) var app = builder.Build(); // Enable Swagger UI only in Development environment to test API endpoints interactively if (app.Environment.IsDevelopment()) { app.UseSwagger(); // Enable Swagger middleware to generate swagger.json app.UseSwaggerUI(); // Enable Swagger UI middleware to visualize API docs } // Register the custom global error handling middleware in the pipeline // It will catch exceptions from downstream middleware/endpoints app.UseMiddleware<ErrorHandlerMiddleware>(); // -------------------- Define Minimal API Endpoints -------------------- // GET /employees - Fetch all employees app.MapGet("/employees", async (IEmployeeRepository repo, ILogger<Program> logger) => { logger.LogInformation("Fetching all employees asynchronously"); var employees = await repo.GetAllEmployeesAsync(); return Results.Ok(employees); }); // GET /employees/{id} - Fetch employee by ID app.MapGet("/employees/{id}", async (int id, IEmployeeRepository repo, ILogger<Program> logger) => { logger.LogInformation($"Fetching employee with ID: {id} asynchronously"); var employee = await repo.GetEmployeeByIdAsync(id); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // POST /employees - Create a new employee app.MapPost("/employees", async (Employee newEmployee, IEmployeeRepository repo, ILogger<Program> logger) => { if (!ValidationHelper.TryValidate(newEmployee, out var errors)) { return Results.BadRequest(new { Message = "Validation Failed", Errors = errors.Select(e => e.ErrorMessage) }); } var createdEmployee = await repo.AddEmployeeAsync(newEmployee); logger.LogInformation($"Employee created with ID {createdEmployee.Id} asynchronously"); return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee); }); // PUT /employees/{id} - Update an existing employee app.MapPut("/employees/{id}", async (int id, Employee updatedEmployee, IEmployeeRepository repo, ILogger<Program> logger) => { if (!ValidationHelper.TryValidate(updatedEmployee, out var errors)) { return Results.BadRequest(new { Message = "Validation Failed", Errors = errors.Select(e => e.ErrorMessage) }); } var employee = await repo.UpdateEmployeeAsync(id, updatedEmployee); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // DELETE /employees/{id} - Delete an employee by ID app.MapDelete("/employees/{id}", async (int id, IEmployeeRepository repo, ILogger<Program> logger) => { var deleted = await repo.DeleteEmployeeAsync(id); return deleted ? Results.NoContent() : Results.NotFound(); }); // Start the web server and listen for incoming HTTP requests app.Run(); } } }
Asynchronous programming allows our web API to handle more requests concurrently and efficiently by releasing threads during I/O-bound operations. This makes our API more scalable, responsive, and better able to utilize server resources, ultimately delivering faster responses and supporting more users without needing more hardware.
In the next article, I will discuss ASP.NET Core Minimal API using Entity Framework Core with Examples. In this article, I explain how to Implement Asynchronous Programming in ASP.NET Core Minimal API with Examples. I hope you enjoy this article.