Open Weather: Efficient Async Data Fetching with Caching

This demo showcases how to use asynchronous programming and caching in .NET 8 to efficiently fetch and display weather data from the OpenWeatherMap API. By leveraging async methods, in-memory caching, and robust error handling, this approach ensures a responsive and reliable user experience.

Weather for Dallas
Observed at 9/19/2024 11:01:55 AM
Temperature
88.8 ° F
Humidity
63.0 %
Wind
SW 8.1 mph
Conditions
Clear

Fetch time: 9/19/2024 4:07:11 PM | Time Since Fetch: 00:00:00.0090014

What This Demo Demonstrates

This demo showcases the integration of asynchronous programming and custom caching strategies in .NET applications, specifically through the use of a custom MemoryCacheManager implementation. By utilizing asynchronous data retrieval with caching, the application remains responsive and performs optimally when accessing external services like the OpenWeatherMap API. The MemoryCacheManager not only tracks cache keys efficiently but also offers advanced cache management capabilities, such as eviction handling and in-memory locks, ensuring high performance and reduced API call frequency, ultimately lowering costs.

Key Benefits:

  • Improved Performance: Asynchronous programming keeps the application responsive by preventing main thread blocking, allowing for smooth user interactions.
  • Optimized Resource Usage: The MemoryCacheManager minimizes unnecessary API calls, saving bandwidth and reducing latency by efficiently managing cached data.
  • Enhanced Resilience: Advanced error handling and in-memory locking capabilities allow the application to gracefully handle failed API calls and race conditions, maintaining stability.
  • Custom Cache Management: The custom MemoryCacheManager provides flexibility in managing cache entries, including setting cache expirations and tracking key existence, which is crucial for maintaining data relevance.

Key Code Samples:

Asynchronous Data Retrieval:

public async Task Index(string? location = null)
{
    location ??= "Dallas";  // Default location if none provided
    var myList = GetCurrentWeatherList();

    if (!myList.Any(w => w.Location?.Name == location))
    {
        var conditions = await _weatherClient.GetCurrentWeatherAsync(location);
        if (!conditions.Success)
        {
            conditions.ErrorMessage = $"{conditions.ErrorMessage} received for location: {location}";
            _logger.LogError(conditions.ErrorMessage);
        }
        myList = AddCurrentWeatherList(conditions);
    }
    else
    {
        _logger.LogInformation("Location {location} is already in the list", location);
    }
    return View(myList);
}

This code demonstrates the use of asynchronous programming to fetch current weather data without blocking the main thread, providing a seamless user experience.

Custom Caching with MemoryCacheManager:

private List GetCurrentWeatherList()
{
    return _cacheManager.Get(CurrentWeatherListCacheKey, () =>
    {
        return new List();
    }, 30); // Cache time in minutes
}

private List AddCurrentWeatherList(CurrentWeather currentWeather)
{
    var theList = GetCurrentWeatherList();
    if (!theList.Any(w => w.Location?.Name == currentWeather?.Location?.Name))
    {
        theList.Add(currentWeather);
        _cacheManager.Set(CurrentWeatherListCacheKey, theList, 30); // Cache time in minutes
    }
    return theList;
}

These methods illustrate how the custom MemoryCacheManager is used to efficiently manage caching of weather data, reducing unnecessary API calls and improving application performance.

Combining Async with Polly for Resilience:

public async Task GetWeatherWithRetryAsync(string location)
{
    var policy = Policy
        .Handle()
        .RetryAsync(3, (exception, retryCount) =>
        {
            _logger.LogWarning($"Retry {retryCount} for {location} due to {exception.Message}");
        });

    return await policy.ExecuteAsync(() => _weatherClient.GetCurrentWeatherAsync(location));
}

Using Polly with asynchronous methods helps manage retries and transient faults, enhancing the resilience of the application.

Alternative Approaches:

While this demo utilizes an in-memory caching strategy with the MemoryCacheManager, other alternatives include distributed caching solutions like Redis or SQL-based caching for scenarios requiring persistence across multiple servers. For cloud environments, solutions like Azure Cache for Redis provide high availability, scalability, and integrated security features. Additionally, leveraging built-in resilience features in tools such as Azure API Management can further enhance API performance and reliability through retry policies and fault tolerance mechanisms.

Best Practices:

  • Set Appropriate Cache Expirations: Define sensible expiration times to keep cached data up-to-date and relevant, avoiding stale information.
  • Monitor and Optimize Caching Strategies: Regularly review API usage patterns and adjust caching strategies to maintain performance and cost-efficiency.
  • Combine Async Operations with Polly: Utilize Polly to implement retries, circuit breakers, and fallback strategies alongside asynchronous methods to enhance the resilience of your application.