Creating an Web API in ASP.NET Core

By FoxLearn 12/19/2024 6:38:35 AM   98
This article covers the fundamentals of asynchronous (async) programming and its advantages in Web APIs.

It explains why async APIs are necessary and how they improve performance by allowing non-blocking operations, enabling better resource utilization.

Both .NET and .NET Core applications can implement asynchronous programming using the async and await keywords. While the focus is on applying asynchronous programming to an ASP.NET Core Web API, the concepts discussed are applicable to other .NET applications as well.

In Web API, asynchronous programming enhances scalability by allowing an application to handle more concurrent requests, rather than directly improving speed. Using async and await does not reduce the time taken for individual tasks (e.g., database operations or external API calls), but it enables the application to release the thread while waiting for a response, allowing other requests to be processed.

When a Web API is deployed on IIS, each application runs in its own worker pool with a fixed number of threads. If concurrent requests exceed the available threads, they enter a pending state. To avoid this, scaling options like vertical or horizontal scaling can be applied. Vertical scaling involves improving the server's resources (e.g., CPU, memory) or optimizing the application to use resources more efficiently, which asynchronous programming helps achieve by increasing the system’s ability to handle more requests.

Synchronous Requests

When a request arrives at the server, it is assigned a thread from the thread pool, which has a fixed number of threads that are configured at the application's startup but cannot be changed at runtime. The application must manage throughput based on the number of available threads. If the number of concurrent requests exceeds the available threads, additional requests must wait in a queue until a thread becomes free.

If the queue limit is exceeded, new requests will result in "service unavailable" errors. Similarly, if a request is pending too long, clients may experience timeouts. Threads that are waiting for long-running tasks, like database queries or HTTP calls, become blocked during this wait, effectively doing nothing.

Asynchronous programming helps by allowing these threads to be freed up while waiting for tasks to complete, enabling better utilization of resources and improving the ability to handle concurrent requests.

Asynchronous Requests

In this scenario, there is still a fixed number of threads in the thread pool to handle incoming requests. When the number of concurrent requests exceeds the available threads, additional requests go into a wait state. The key difference with asynchronous programming is that threads are not blocked by long-running tasks.

When a request arrives, it is assigned a thread from the pool. If the request involves a long-running task (like a database query, HTTP call, or I/O operation), instead of blocking the thread, the task is awaited, allowing the thread to be released back into the pool to process other requests. Once the task completes, the thread is re-assigned to handle the task's response.

This design significantly improves scalability by allowing the application to handle more concurrent requests with the same resources, making it more responsive to users.

Advantages of Asynchronous Programming

Asynchronous programming improves the scalability of an application by enabling it to handle more requests with the same server resources. This is achieved by not blocking threads for long-running tasks, allowing threads to be released back to the thread pool to process other requests while waiting for these tasks to complete.

Handling more concurrent requests leads to faster responses for users, reducing the likelihood of timeouts or server errors (e.g., "Service Unavailable" 503). Additionally, there is an indirect performance improvement: by managing more requests, the average response time decreases, resulting in quicker responses and an enhanced overall user experience.

How to use async await in ASP.NET Core

In ASP.NET Core C#, asynchronous programming is implemented using the async and await keywords. To make a method asynchronous, the async keyword is added before the method's return type.

public async Task SaveChanges(Customer obj)
{
    _dbcontext.Customers.Add(obj);
    await _dbcontext.SaveChanges();
}

Before implementing asynchronous techniques in your code, ask yourself if the execution of your code will need to wait for a task to complete before continuing. If the answer is yes, then asynchronous programming should be considered. Instead of waiting for the task to finish, you can start the task and await its response.

Asynchronous techniques are particularly useful for I/O-bound tasks such as file system operations (reading/writing files), database operations (add, update, delete, or query), and network-based HTTP calls to third-party APIs (e.g., Google, Facebook, SMS services, email).

Additionally, asynchronous techniques can also be applied to CPU-bound tasks, such as processing large collections of data, performing heavy calculations (e.g., insurance premium calculations, interest calculations for long-term loans), or processing large volumes of time-series data from logs.

How to Create an ASP.NET Core Web API Project

Open Visual Studio, then create a new project, select the "ASP.NET Core Web API" project type and name it "SamplesWebAPI".

To demonstrate an async Web API with ASP.NET Core, we will use Entity Framework Core, as it supports async methods for data operations.

You can either run the specified commands in the Package Manager or install the necessary NuGet packages through the NuGet Package Manager.

Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools

1. Add Database Entity

We will create a database entity class for the customer in the DBEntities/Customer.cs file, as shown below.

public class Customer
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

2. Add Database Context

The database context class is responsible for coordinating Entity Framework operations for a specific database entity, which in this case is the Customer entity. This class should inherit from the DbContext class provided by Entity Framework and define the entities included in the Web API. It creates a DbSet property for the Customer entity set, where an entity set typically represents a database table, and an entity represents a row in that table.

We will define the interface for the database context class for the Customer entity under Interfaces/IApplicationDbContext.cs, as shown below.

public interface IApplicationDbContext
{
    DbSet<Customer>? Customers { get; set; }
    Task<int> SaveChangesAsync();
}

Next, We will create the database context class for the Customer entity in the DBContext/ApplicationDbContext.cs file, as shown below.

public class ApplicationDbContext : DbContext, IApplicationDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Customer>? Customers { get; set; }

    public new async Task<int> SaveChangesAsync()
    {
        return await base.SaveChangesAsync();
    }
}

The database table created will have the same name as the DbSet property name in the database context class.

3. Add Connection String to appsettings.json file

Specify the SQL Server connection string in the appsettings.json file. In this case, we are using a local database (localdb), which is a lightweight version of the SQL Server Express database engine. Add the appropriate connection string entry to the appsettings.json file as shown below.

"ConnectionStrings": {
  "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}

4. Register Database Context

To enable dependency injection of the DbContext service into controllers or other service classes, you need to configure the database context as a service. This is done in the Program.cs file, as shown in the provided code. By doing this, the DbContext can be injected into the constructor of the required classes.

var configuration = new ConfigurationBuilder()
               .SetBasePath(Directory.GetCurrentDirectory())
               .AddJsonFile("appsettings.json")
               .Build();

builder.Services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
        configuration.GetConnectionString("DefaultConnection"),
        ef => ef.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
builder.Services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());

5. Add Migrations

To automate migrations from the Entity Framework classes, you need to run the add-migration command.

To create the database based on these migrations, run the update-database command in the Package Manager Console.

Execute the following commands in the Package Manager Console as shown below.

add-migration Customers
update-database

The above commands will create the database on the specified database server using the connection string from the appsettings.json file. It will also create tables in the new database based on the DbSet objects defined in the DbContext.

6. Add Customer Service

This allows the service to use the application database context to implement a method that retrieves a list of all customers from the database.

public interface ICustomerService
{
    List<Customer> GetCustomerList();
    Task<List<Customer>> GetCustomerListAsync();
}

The implementation for the CustomerService has been added in the Services/CustomerService.cs file, as shown below. This service contains the logic for interacting with the database and handling customer related operations.

public class CustomerService : ICustomerService
{
    readonly IApplicationDbContext _applicationDbContext;

    public CustomerService(IApplicationDbContext applicationDbContext)
    {
        _applicationDbContext = applicationDbContext;
    }

    public List<Customer> GetCustomerList()
    {
        return _applicationDbContext.Customers.ToList<Customer>();
    }

    public async Task<List<Customer>> GetCustomerListAsync()
    {
        return await _applicationDbContext.Customers.ToListAsync<Customer>());
    }
}

In the CustomerService code, two methods are added to retrieve a list of all customers from the database:

  1. GetCustomerList: This method runs synchronously, meaning the thread will be blocked until the database call completes.

  2. GetCustomerListAsync: This is the asynchronous version of the GetCustomerList method. It uses the async keyword in its definition and await in its body, allowing the thread to remain free and available in the thread pool to handle other requests while waiting for the database call to complete. Once the call completes, the thread is re-assigned from the pool and execution continues from where the task was awaited.

In the asynchronous method, the ToListAsync() method from the Microsoft.EntityFrameworkCore namespace is used, which is the async version of the synchronous ToList() method.

When using async methods, avoid using Result() or Wait() as they block the thread until the operation completes, which goes against the purpose of using async code for I/O operations.

The CustomerService has been registered in the dependency injection container so that it can be injected into the controller via the constructor. To register the CustomerService, add the following line of code in the Program.cs file. This ensures that the service is available for dependency injection throughout the application.

builder.Services.AddTransient<ICustomerService, CustomerService>();

7. Add Customer Controller

The CustomerController has been added to expose a GET action for retrieving all customers from the database. The CustomerService is injected into the controller via the constructor using dependency injection. The controller calls methods from the CustomerService to implement the functionality.

The controller supports both synchronous and asynchronous action methods, utilizing the corresponding synchronous and asynchronous methods in the CustomerService to fetch the list of customers from the database.

[Route("api/[controller]")]
[ApiController]
public class CustomerController : ControllerBase
{
    readonly ICustomerService? _customerService;

    public CustomerController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    // POST api/<CustomerController>
    [HttpGet("GetCustomerList")]
    public List<Customer> GetCustomers()
    {
        return _customerService.GetCustomerList();
    }

    // POST api/<CustomerController>
    [HttpGet("GetCustomersAsync")]
    public async Task<List<Customer>> GetCustomersAsync()
    {
        return await _customerService.GetCustomerListAsync();
    }
}

In the async action method, the await keyword is used to extract the result from the asynchronous operation. Once the result is obtained, it is validated for success or failure. After validation, the code execution continues, executing the statements following the await statement.

An async method can contain multiple await statements, depending on the logic within the method. Each await will pause the method’s execution until the awaited task completes, allowing other tasks to be processed in the meantime.