Performance Optimization

Learning Objectives

  • Understand how asynchronous API endpoints improve scalability and responsiveness.
  • Learn how to use caching techniques (in-memory and distributed caching) to optimize the performance of API requests.
  • Understand connection pooling and how it can help optimize database performance.

Asynchronous API Endpoints

What are Asynchronous API Endpoints? Asynchronous programming allows you to handle requests without blocking the main thread. This is useful in scenarios where waiting for a response (e.g., reading from a database) could take time. In .NET Core, you use the async/await keywords to create asynchronous methods.

Why Use Asynchronous Endpoints?

  • Non-blocking: The server can handle other requests while waiting for a response, improving responsiveness.
  • Scalability: Improves the capacity of the server to handle multiple requests concurrently.

Example: Asynchronous API Endpoint for Getting a Product:

[HttpGet("{id}")]
public async Task<IActionResult> GetProductAsync(int id)
{
    var product = await _productService.GetProductByIdAsync(id);
    if (product == null)
    {
        return NotFound();
    }

    return Ok(product);
}
  • Task<IActionResult>: Indicates that the method is asynchronous and returns a result in the future.
  • await: Waits for the operation to complete without blocking the main thread.

Real-Life Example: In an e-commerce application, fetching product details or pricing data from an external API is an example where asynchronous programming can improve the scalability and response time of your server.

Caching

What is Caching? Caching is storing frequently used data in a temporary storage location so that future requests can be served faster. This is particularly useful for GET requests that return data that doesn’t change frequently.

Types of Caching:

  1. In-Memory Caching: Stores data directly in the application's memory, suitable for small-scale caching.
  2. Distributed Caching: Stores data in an external store (e.g., Redis) to share across multiple instances of the application, often used in distributed environments.

Example: In-Memory Caching in ASP.NET Core:

Add In-Memory Caching in Program.cs:

builder.Services.AddMemoryCache();

Use In-Memory Cache in a Controller:

private readonly IMemoryCache _cache;

public ProductController(IMemoryCache cache)
{
    _cache = cache;
}

[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
    string cacheKey = $"Product_{id}";
    if (!_cache.TryGetValue(cacheKey, out Product product))
    {
        product = _productService.GetProductById(id);
        if (product == null)
        {
            return NotFound();
        }

        var cacheOptions = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
        };
        _cache.Set(cacheKey, product, cacheOptions);
    }

    return Ok(product);
}
  • TryGetValue: Checks if the product is available in the cache. If not, it fetches from the service and stores it in the cache.
  • MemoryCacheEntryOptions: Configures cache expiration.

Example: Distributed Caching with Redis:

Add Redis Caching in Program.cs:

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "ECommerceCache_";
});

Use Redis Cache in a Controller:

private readonly IDistributedCache _cache;

public ProductController(IDistributedCache cache)
{
    _cache = cache;
}

[HttpGet("{id}")]
public async Task<IActionResult> GetProductAsync(int id)
{
    string cacheKey = $"Product_{id}";
    string cachedProduct = await _cache.GetStringAsync(cacheKey);

    if (string.IsNullOrEmpty(cachedProduct))
    {
        var product = await _productService.GetProductByIdAsync(id);
        if (product == null)
        {
            return NotFound();
        }

        cachedProduct = JsonSerializer.Serialize(product);
        await _cache.SetStringAsync(cacheKey, cachedProduct, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
        });
    }

    var productResult = JsonSerializer.Deserialize<Product>(cachedProduct);
    return Ok(productResult);
}
  • IDistributedCache: Used for interacting with Redis.
  • Serialization: Converts the product to/from JSON strings for caching.

Real-Life Example: In a financial system, caching can be used to store frequently requested data such as exchange rates to avoid querying the database or external services repeatedly.

Connection Pooling

What is Connection Pooling? Connection pooling is a technique to manage database connections by reusing active connections instead of opening a new one each time a request is made. This improves the performance and scalability of the application by reducing the overhead of creating and closing database connections.

How Does Connection Pooling Work in ASP.NET Core?

  • SQL Server Connection Pooling is enabled by default when using SqlConnection. It allows the application to keep a pool of active connections, and when a request is made, it reuses an available connection from the pool.

Example: Using SQL Connection Pooling:

public async Task<Product> GetProductByIdAsync(int id)
{
    string connectionString = "YourDatabaseConnectionString";
    using (var connection = new SqlConnection(connectionString))
    {
        await connection.OpenAsync();
        string query = "SELECT * FROM Products WHERE Id = @Id";
        using (var command = new SqlCommand(query, connection))
        {
            command.Parameters.AddWithValue("@Id", id);
            using (var reader = await command.ExecuteReaderAsync())
            {
                if (await reader.ReadAsync())
                {
                    return new Product
                    {
                        Id = (int)reader["Id"],
                        Name = (string)reader["Name"],
                        Price = (decimal)reader["Price"]
                    };
                }
                return null;
            }
        }
    }
}
  • Connection Pooling is handled automatically by the connection string. The connection is opened, used, and then returned to the pool for reuse.

Real-Life Example: In a user management system, pooling allows reusing connections for multiple requests to the user database to improve the overall response time, especially during peak loads.

Examples

Simple Example: Asynchronous Endpoint for Adding a Product:

[HttpPost]
public async Task<IActionResult> AddProductAsync([FromBody] Product product)
{
    await _productService.AddProductAsync(product);
    return CreatedAtAction(nameof(GetProductAsync), new { id = product.Id }, product);
}
  • Real-Life Example: In an e-commerce application, adding a product to the catalog should be non-blocking to ensure a good user experience.

Simple Example: Caching Product List with In-Memory Cache:

[HttpGet("all")]
public IActionResult GetAllProducts()
{
    var cacheKey = "AllProducts";
    if (!_cache.TryGetValue(cacheKey, out List<Product> products))
    {
        products = _productService.GetAllProducts();
        _cache.Set(cacheKey, products, TimeSpan.FromMinutes(10));
    }

    return Ok(products);
}
  • Real-Life Example: In an e-commerce site, caching all products helps improve the response time when users browse the catalog.

Simple Example: SQL Connection Pooling for User Authentication:

public async Task<User> AuthenticateUserAsync(string username, string password)
{
    string connectionString = "YourDatabaseConnectionString";
    using (var connection = new SqlConnection(connectionString))
    {
        await connection.OpenAsync();
        string query = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";
        using (var command = new SqlCommand(query, connection))
        {
            command.Parameters.AddWithValue("@Username", username);
            command.Parameters.AddWithValue("@Password", password);
            using (var reader = await command.ExecuteReaderAsync())
            {
                if (await reader.ReadAsync())
                {
                    return new User
                    {
                        Id = (int)reader["Id"],
                        Username = (string)reader["Username"]
                    };
                }
                return null;
            }
        }
    }
}
    • Real-Life Example: In a user management system, using connection pooling helps speed up authentication requests, especially during high-traffic periods.

Key Takeaways

  • Asynchronous Endpoints: Improve scalability by allowing non-blocking request handling.
  • Caching: Use in-memory or distributed caching to store frequently requested data, improving performance.
  • Connection Pooling: Reuse database connections to reduce the overhead of opening and closing connections frequently, which optimizes performance.

Practical Questions

  1. How do asynchronous API endpoints help improve application performance?
  2. What are the differences between in-memory and distributed caching, and when would you use each?
  3. How does connection pooling improve the efficiency of database operations?

New Concepts in .NET 8

New in .NET 8:

  • Improved Asynchronous Handling: .NET 8 brings enhancements in the async/await mechanism that further optimize the use of threads, improving scalability and response time.
  • Native AOT Improvements: Allows more optimized code paths for asynchronous I/O operations, improving the performance of APIs when deployed.

ניווט במאמר

מאמרים אחרונים

Weekly Tutorial