DEV Community

Cover image for Improving Performance in .NET Applications with Asynchronous Programming and Caching
Leandro Veiga
Leandro Veiga

Posted on

Improving Performance in .NET Applications with Asynchronous Programming and Caching

In today's fast-paced digital world, application performance is a crucial aspect of ensuring user satisfaction. One of the most effective ways to enhance performance in .NET applications is by leveraging asynchronous programming and caching. These techniques help improve response times, reduce resource consumption, and make applications more scalable.

In this blog post, we’ll dive into how asynchronous programming and caching can be used to optimize the performance of your .NET applications.

Why Asynchronous Programming?

Asynchronous programming is essential when it comes to handling operations that can be slow or blocking, such as:

  • Accessing databases
  • Making network calls (e.g., external APIs)
  • Performing file I/O operations

By using asynchronous programming, we can allow the application to handle other tasks while waiting for these operations to complete, improving the overall responsiveness of the system.

Key Benefits of Asynchronous Programming:

  • Non-blocking operations: Asynchronous code allows the application to keep running other processes without waiting for slow operations to complete.
  • Better resource utilization: Threads can be used more efficiently, reducing idle times.
  • Scalability: Applications can handle a larger number of requests simultaneously.

Asynchronous Programming in .NET

In .NET, asynchronous programming is based on the async and await keywords, making it easy to write non-blocking code. These features are built on the Task-based asynchronous pattern (TAP).

Example: Synchronous vs Asynchronous

Synchronous Code:

public string GetData()
{
    // Simulate a time-consuming operation
    Thread.Sleep(5000);  // Blocks the thread for 5 seconds
    return "Data retrieved";
}
Enter fullscreen mode Exit fullscreen mode

In this example, the application is blocked for 5 seconds, unable to perform other tasks.

Asynchronous Code:

public async Task<string> GetDataAsync()
{
    await Task.Delay(5000);  // Non-blocking delay
    return "Data retrieved asynchronously";
}
Enter fullscreen mode Exit fullscreen mode

With the asynchronous version, the application is not blocked and can handle other requests or tasks during the 5-second wait time.

Best Practices for Asynchronous Programming

  1. Avoid async void: Always return Task or Task<T>, except for event handlers. Using async void can lead to unhandled exceptions and unpredictable behavior.

  2. Use ConfigureAwait(false): When writing library code, use .ConfigureAwait(false) to prevent deadlocks caused by capturing the original synchronization context.

  3. Parallelize independent tasks: If multiple tasks don’t depend on each other, run them in parallel using Task.WhenAll().

    var task1 = GetDataAsync();
    var task2 = GetMoreDataAsync();
    await Task.WhenAll(task1, task2);  // Both tasks run concurrently
    

Caching for Performance

Caching is one of the most effective techniques for improving performance. By storing frequently accessed data in a cache, applications can retrieve it much faster than from the original data source.

Benefits of Caching:

  • Reduced latency: Cached data can be accessed much faster than fetching it from a database or external API.
  • Reduced load: Caching reduces the load on databases or external services by minimizing the number of requests.
  • Improved scalability: With fewer requests going to the backend services, your application can handle a higher volume of traffic.

Types of Caching in .NET

  1. In-memory Caching: Suitable for storing data that can be reused during the application’s runtime. This type of caching is ideal for web or API responses that don’t frequently change.

  2. Distributed Caching: Data is cached across multiple servers, making it suitable for large-scale applications with multiple instances. Redis is a popular option for distributed caching.

Implementing In-memory Caching with .NET

.NET provides built-in support for in-memory caching through the MemoryCache class. Here’s an example of how to implement it:

public class MyService
{
    private readonly IMemoryCache _cache;

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

    public string GetCachedData()
    {
        string cachedData;
        if (!_cache.TryGetValue("myKey", out cachedData))
        {
            // Data is not in cache, so retrieve it
            cachedData = "Data from database or API";

            // Set cache options
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(5));

            // Save data in cache
            _cache.Set("myKey", cachedData, cacheEntryOptions);
        }

        return cachedData;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The TryGetValue method checks if the data is already in the cache.
  • If the data is not in the cache, it fetches the data and stores it in the cache with an expiration policy.

Implementing Distributed Caching with Redis

Distributed caching is essential when dealing with applications that scale horizontally across multiple servers. Redis is a popular in-memory data store that can be used for distributed caching.

To use Redis in your .NET application, you’ll first need to install the Microsoft.Extensions.Caching.StackExchangeRedis package.

Configuring Redis Cache:

public void ConfigureServices(IServiceCollection services)
{
    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = "localhost:6379";  // Redis server address
    });
}
Enter fullscreen mode Exit fullscreen mode

Using Redis Cache:

public class MyService
{
    private readonly IDistributedCache _cache;

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

    public async Task<string> GetCachedDataAsync()
    {
        string cachedData = await _cache.GetStringAsync("myKey");
        if (string.IsNullOrEmpty(cachedData))
        {
            // Data not in cache, fetch from source
            cachedData = "Data from database or API";

            // Save data in Redis with an expiration time
            await _cache.SetStringAsync("myKey", cachedData, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            });
        }

        return cachedData;
    }
}
Enter fullscreen mode Exit fullscreen mode

When to Use Caching

  • Static or infrequently changing data: Cache data that doesn’t change frequently, such as configuration settings, reference data, or product catalogs.
  • Heavy queries or slow external requests: Cache results from complex database queries or slow API calls.
  • Session or authentication tokens: Store session data or authentication tokens for faster access.

Combining Asynchronous Programming with Caching

Asynchronous programming and caching can be combined to further improve performance. For example, you can asynchronously fetch data from the cache and, if the cache misses, asynchronously retrieve the data from the original source and store it in the cache.

public async Task<string> GetDataAsync()
{
    string cachedData = await _cache.GetStringAsync("myKey");
    if (string.IsNullOrEmpty(cachedData))
    {
        cachedData = await FetchDataFromApiAsync();  // Asynchronous API call

        await _cache.SetStringAsync("myKey", cachedData, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
        });
    }

    return cachedData;
}
Enter fullscreen mode Exit fullscreen mode

In this example, both the data retrieval and caching operations are performed asynchronously, ensuring that the application remains responsive and efficient.

Conclusion

By leveraging asynchronous programming and caching in your .NET applications, you can significantly improve performance and scalability. Asynchronous programming allows your application to handle tasks more efficiently, while caching reduces the load on your backend services and speeds up response times.

If you're building high-traffic applications or services that rely on external data, these techniques are essential to maintaining performance and delivering a great user experience.

Top comments (0)