DEV Community

Cover image for Easy way to cache data.
Serhii Korol
Serhii Korol

Posted on

Easy way to cache data.

Hi, community. In this article, I want to show how to cache your data from an HTTP request. It'll be helpful when you have restricted resources or payment external API or your data isn't updating frequently.

Let's create a simple empty Web API project, and it would be fine if you delete all redundant code.

You also need register or login in the https://openweathermap.org/. We'll be use forecast API.

Let's begin with the models. I'll ask you to create this model, and it needs for a result.

public record WeatherForecast
{
    //today current date
    public DateOnly Date { get; set; }

    //temperature in Celsius
    public double TemperatureC { get; set; }

    //compute from Celsius to Fahrenheit
    public double TemperatureF => 32 + TemperatureC / 0.5556;

    //Any message 
    public string? Summary { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Also, you need to create a model for HTTP response. I just grabbed from the API documentation JSON document and with ChatGPT help generated models. However you can copy it here:

public class Coord
{
    public double lon { get; set; }
    public double lat { get; set; }
}

public record Weather
{
    public int id { get; set; }
    public string main { get; set; }
    public string description { get; set; }
    public string icon { get; set; }
}

public record Main
{
    public double temp { get; set; }
    public double feels_like { get; set; }
    public double temp_min { get; set; }
    public double temp_max { get; set; }
    public int pressure { get; set; }
    public int humidity { get; set; }
    public int sea_level { get; set; }
    public int grnd_level { get; set; }
}

public record Wind
{
    public double speed { get; set; }
    public int deg { get; set; }
    public double gust { get; set; }
}

public record Rain
{
    public double _1h { get; set; }
}

public record Clouds
{
    public int all { get; set; }
}

public record Sys
{
    public int type { get; set; }
    public int id { get; set; }
    public string country { get; set; }
    public long sunrise { get; set; }
    public long sunset { get; set; }
}

public record OpenWeatherMapResponse
{
    public Coord coord { get; set; }
    public List<Weather> weather { get; set; }
    [JsonPropertyName("base")] public string Base { get; set; }
    public Main main { get; set; }
    public int visibility { get; set; }
    public Wind wind { get; set; }
    public Rain rain { get; set; }
    public Clouds clouds { get; set; }
    public long dt { get; set; }
    public Sys sys { get; set; }
    public int timezone { get; set; }
    public int id { get; set; }
    public string name { get; set; }
    public int cod { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Following step, you need paste this service:

public interface IWeatherService
{
    Task<WeatherForecast?> GetWeatherByCity(string city);
}

public class WeatherService : IWeatherService
{
    //key from openweathermap.org
    private const string ApiKey = "60bdfb3ef219dcd99a4927f44e8b65bd";
    private readonly IHttpClientFactory _httpClientFactory;

    public WeatherService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<WeatherForecast?> GetWeatherByCity(string city)
    {
        ArgumentNullException.ThrowIfNull(city);
        var client = _httpClientFactory.CreateClient("weather");
        var url = $"https://api.openweathermap.org/data/2.5/weather?q={city}&units=metric&appid={ApiKey}";

        var response = await client.GetAsync(url);
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }

        var weather = await response.Content.ReadFromJsonAsync<OpenWeatherMapResponse>();
        return new WeatherForecast
        {
            TemperatureC = weather!.main.temp,
            Date = DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(weather.dt).LocalDateTime),
            Summary = weather.main.temp < 20
                ? $"The weather is ugly in {weather.name}"
                : $"The weather is fine in {weather.name}"
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

If you can't find the key, just open your profile as shown in the picture:

key

Now we are approaching the main topic of this article. Currently, the HTTP request gets data, and I can make requests every second. However, the API has restrictions by request count. This issue was resolved simply by using caching. I'll show how to cache data on the HttpClient level.

Copy this code:

public class HttpCacheHandler : DelegatingHandler
{
    private readonly IMemoryCache _cache;
    public HttpCacheHandler(IMemoryCache cache)
    {
        _cache = cache;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var queryString = HttpUtility.ParseQueryString(request.RequestUri!.Query);
        var query = queryString["q"];
        var units = queryString["units"];
        var key = $"{query}-{units}";
        var cached = _cache.Get<string>(key);
        if (cached is not null)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("The response from the cache");
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(cached)
            };
        }
        var response = await base.SendAsync(request, cancellationToken);
        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        _cache.Set(key, content, TimeSpan.FromMinutes(1));
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"The key: {key}, the content: {content}");
        Console.WriteLine("The cache was updated");
        return response;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I overrode the SendAsync method and added IMemoryCache. I set the cache lifetime. For convenience, I set one minute, but in real life, you might need more intervals. We check if the data exists, and then we return it. If not, we call API and update the cache.

Before we'll check this out. We need to register services.

builder.Services.AddMemoryCache();
builder.Services.AddHttpClient("weather")
    .AddHttpMessageHandler(provider => new HttpCacheHandler(provider.GetRequiredService<IMemoryCache>()));

builder.Services.AddSingleton<IWeatherService, WeatherService>();
builder.Services.AddScoped<HttpCacheHandler>();
Enter fullscreen mode Exit fullscreen mode

And the final touch you need create minimal API.

app.MapGet("weather/{city}", async (string city, IWeatherService weatherService) =>
{
    var weatherForecast = await weatherService.GetWeatherByCity(city);

    return weatherForecast != null ? Results.Ok(weatherForecast) : Results.NotFound($"Weather data not found for {city}");
});
Enter fullscreen mode Exit fullscreen mode

Let's run the project and make the first request in Swagger.

swagger

After executing you get this:

response

If you'll to look at the debug console, you'll see that the cache was updated.

update

If rapidly repeat this request, you'll see that the response came from the cache.

cache

Conclusion

It can be helpful for reducing API call count to third-party endpoints and also it can be useful for tests when you can reuse data.

I hope this information was interesting and helpful for you. Make subscriptions and put likes. See you in the next article and happy coding!

The source code.

Buy Me A Beer

Top comments (0)