ASP.NET Core Basics: Exploring Cache In-Memory

Using in-memory caching improves the performance and scalability of web applications. In this blog post, we will check out how to implement in-memory caching and see the benefits it offers in practice.


This content originally appeared on Telerik Blogs and was authored by Assis Zang

Using in-memory caching improves the performance and scalability of web applications. In this blog post, we will check out how to implement in-memory caching and see the benefits it offers in practice.

The use of cache is essential in scenarios where a large amount of data is read, as it makes this data accessible and less expensive compared to traditional methods such as direct access to the database for each request.

ASP.NET Core has excellent mechanisms for dealing with caching. In this post, we will explore in detail what in-memory caching is, how it can be configured and used in ASP.NET Core, and the best practices for managing the cache effectively.

What Is Caching?

In computing, caching is a technique that temporarily stores data in an easily accessible location, which may be in memory or another source.

The main advantage of using a cache is to avoid accessing the standard storage device, such as a database, which can often be costly. Thus, the data is stored in a temporary source that is easier to access, making querying this data faster and cheaper.

Cache usage demonstration

What Is Cache In-Memory?

ASP.NET Core supports several different types of caches. The memory cache is the simplest and is stored in the web server’s memory. The in-memory cache can store any kind of object in key-value pair format.

It’s possible to implement in-memory caching using two approaches:

  1. Using the System.Runtime.Caching/MemoryCache namespace is part of the .NET Framework and provides an API for in-memory caching. It’s a solid option, especially for dealing with legacy code.
  2. Using the Microsoft.Extensions.Caching.Memory namespace is the best approach for new ASP.NET Core applications. It is part of the ASP.NET Core features and provides a more modern and flexible implementation of in-memory caching.

The in-memory cache in ASP.NET Core temporarily stores data in the server’s memory, allowing it to be retrieved quickly without redo operations, which in many cases can be costly.

When data is accessed for the first time, it is normally fetched from slower sources, such as a database or an external service, and then a copy of that data is stored in cache memory. If the same data is requested again, the system checks whether it is in the cache. If so, the data is delivered quickly from the cache, thus avoiding searching the sources.

Finally, it is possible to use cache expiration or invalidation policies that act periodically to update or remove data from the cache, for data consistency and allowing only the most relevant data to take up space.

Implementing In-Memory Caching

In this post, we will create a simple API to manage temperature data. We will use SQL Server to record and retrieve this data, and then we will check the performance of queries using the in-memory cache.

You can access the complete code here: Weather Forecast API Source code.

Prerequisites

You must have the latest version of .NET. This post uses Version 8.0.

You also need a pre-configured local SQL Server connection. You may use any other database, but this post’s example uses SQL Server together with the Entity Framework Core.

Creating the Application and Installing the Dependencies

To create the sample application, you can use the following command:

dotnet new web -n WeatherForecastAPI

To install the NuGet packages you can use the commands below:

dotnet add package Microsoft.Extensions.Caching.Memory
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Bogus

Creating the Entity

Open the project with your favorite IDE. Create a new folder called “Models” and inside it create the following class:

namespace WeatherForecastAPI.Models;

public class WeatherForecast
{
    public int Id { get; set; }
    public string City { get; set; }
    public double TemperatureC { get; set; }
    public string Summary { get; set; }
    public DateTime Date { get; set; }
}

Creating the Context Class

This example uses EF Core, so we need to create a class to extend the DbContext class. For that, create a new folder called “Data” and inside it create the class below:

using Bogus;
using Microsoft.EntityFrameworkCore;
using WeatherForecastAPI.Models;

namespace WeatherForecastAPI.Data;

public class WeatherForecastDbContext : DbContext
{
    public WeatherForecastDbContext(DbContextOptions<WeatherForecastDbContext> options) : base(options) { }

    public DbSet<WeatherForecast> WeatherForecasts { get; set; }

    public void SeedDatabase(int numberOfRecords)
    {
        if (WeatherForecasts.Any())
            return;

        var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };

        var faker = new Faker<WeatherForecast>()
         .RuleFor(w => w.City, f => f.Address.City())
         .RuleFor(w => w.TemperatureC, f => f.Random.Double(-20, 55))
         .RuleFor(w => w.Summary, f => f.PickRandom(summaries))
         .RuleFor(w => w.Date, f => f.Date.Past(0));

        var weatherData = faker.Generate(numberOfRecords);

        WeatherForecasts.AddRange(weatherData);

        SaveChanges();
    }
}

Note that here we defined a database context called WeatherForecastDbContext, where the method SeedDatabase(int numberOfRecords) is declared to fill the database with dummy data using the Bogus library.

First, the method checks whether there are already records in the weather forecast table. If they exist, the method returns without doing anything, preventing data duplication.

The method then defines an array of weather forecast summaries, such as Freezing, Hot and Mild. Next, the Faker<WeatherForecast> class instance is configured to generate WeatherForecast objects with random values for the city, temperature, summary and date properties. The temperature is generated within a specific range (-20 to 55 degrees Celsius), and the date is a random one in the past.

This data will be used to verify the functioning of the in-memory cache later on.

Creating the Database Configurations and the API Endpoints

The next step is to declare the connection string settings and the endpoint that will implement the use of data in the in-memory cache.

Replace the Program class code with the code below:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using WeatherForecastAPI.Data;
using WeatherForecastAPI.Models;

var builder = WebApplication.CreateBuilder(args);

string connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<WeatherForecastDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddMemoryCache();

var app = builder.Build();

app.MapGet("/weather/{city}", async (string city, WeatherForecastDbContext db, IMemoryCache cache) =>
{
    WeatherForecast? weather = null;

    if (!cache.TryGetValue(city, out weather))
    {
        weather = await db.WeatherForecasts
                          .OrderByDescending(w => w.Date)
                          .FirstOrDefaultAsync(w => w.City == city);

        if (weather != null)
        {
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

            cache.Set(city, weather, cacheEntryOptions);
        }
    }

    return weather == null ? Results.NotFound("Weather data not found for the specified city.") : Results.Ok(weather);
});

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<WeatherForecastDbContext>();

    db.Database.Migrate();

    db.SeedDatabase(10000);
}

app.Run();

Here we defined a configuration to obtain the connection string with the database GetConnectionString("DefaultConnection").

We also added the database context service (WeatherForecastDbContext) to the dependency injection container, configured to use SQL Server with the provided connection string.

Then we added the memory cache service with the configuration: builder.Services.AddMemoryCache().

An API route /weather/{city} is also defined using app.MapGet. This route accepts the name of a city as a parameter and uses the injected database and memory cache services.

Inside the route, the code tries to get the weather forecast from the memory cache. If the prediction is not in the cache, it queries the database.

The most recent forecast for the specified city is obtained from the database by ordering the forecasts by date in descending order and taking the first record that matches the city. If the prediction is found in the database, it is added to the cache with a 10-minute expiration. The method returns the weather forecast if found, or a NotFound response.

The db.Database.Migrate() method applies any pending migrations to make sure the database is up to date, and the db.SeedDatabase(10000) method populates the database with 10,000 dummy weather forecast records if empty.

Note how simple it is to configure the search for data in the cache and insert it if it does not exist.

Generating the Database Migrations

Before running the application and testing the cache search, we need to add the connection string to the project and run the EF Core migrations to generate the database schemas.

So, inside the file appsettings.json add the code below:

"ConnectionStrings": {
    "DefaultConnection": "Server=YOUR_LOCAL_CONNECTION;Database=WeatherForecastDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

Replace the text YOUR_LOCAL_CONNECTION with your local data configuration.

Now, open a terminal at the root of the application and run the EF Core migration commands. Remember that you need to have EF Core installed globally. If you don’t already have it you can use the following command:

dotnet tool install --global dotnet-ef

EF Core migration commands:

  • Create the initial migration
dotnet ef migrations add InitialCreate
  • Update the database
dotnet ef database update

Testing In-Memory Cache Efficiency with Fiddler

Now that we have created and configured the application, we can run it and check the efficiency brought by using the cache. So, just run the application and, through Telerik Fiddler Everywhere, make a request to the route /weather/{city} using one of the cities entered in the database as a parameter. Note that the database will be populated when running the application.

The images below demonstrate requests to the API using Fiddler. When inspecting the details in the Traffic tab, we can see the first request took 332 milliseconds. This is because, in the first request, the data was not in the cache; so the API fetched the data directly from the database, and then added it to the cache.

The second request took just 15 milliseconds, as the API fetched the data from the cache, considerably shortening the time needed to return the data.

Request by Fiddler

Inspecting time first request

Inspecting time second request

To calculate how much more efficient the second request (using the cache) is compared to the first, we can use the following formula for relative efficiency:

Relative Efficiency = First Request Time / Second Request Time

Relative Efficiency = 332 ms / 15 ms

Relative Efficiency = 22.13

This means the second request was approximately 22.13 times more efficient than the first. In the example in the post, a small amount of data was used; however, there are scenarios with significant amounts of data where the cache proves to be extremely efficient in making requests less costly.

Other Options for Handling In-Memory Caching

In addition to the example above, other possibilities exist for dealing with memory cache, such as inserting, updating and removing the cache.

1. Adding Cities with Weather Forecasts to the Cache

The endpoint below lists the different cities and checks if they are in the cache; if not, they are inserted, configuring an expiration time of 30 minutes. Adding items to the cache in advance is useful because it improves application performance by reducing the number of repetitive database queries. This speeds up response time, saves database resources and provides a more efficient user experience.

app.MapGet("/weather/cities", async (WeatherForecastDbContext db, IMemoryCache cache) =>
{
    const string cacheKey = "cities_with_weather";

    if (!cache.TryGetValue(cacheKey, out List<string> cities))
    {
        cities = await db.WeatherForecasts
                         .Select(w => w.City)
                         .Distinct()
                         .ToListAsync();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(30));

        cache.Set(cacheKey, cities, cacheEntryOptions);
    }

    return Results.Ok(cities);
});

2. Update Cache Manually

It is possible to update the cache manually. Suppose you were notified that a city’s data was updated in the database. You can use the endpoint below to update the data in the cache.

app.MapPost("/weather/cache-update/{city}", async (string city, WeatherForecastDbContext db, IMemoryCache cache) =>
{
    var weather = await db.WeatherForecasts
                          .OrderByDescending(w => w.Date)
                          .FirstOrDefaultAsync(w => w.City == city);

    if (weather != null)
    {
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

        cache.Set(city, weather, cacheEntryOptions);
        return Results.Ok($"Cache for city {city} updated.");
    }
    else
        return Results.NotFound("Weather data not found for the specified city.");
});

3. Remove a Specific Item from the Cache

If for some reason you need to remove a specific item from the cache, you could have an endpoint like this:

app.MapDelete("/weather/cache-delete/{city}", (string city, IMemoryCache cache) =>
{
    cache.Remove(city);
    return Results.Ok($"Cache for city {city} removed.");
});

4. Clear All Cache

It is also possible to clear the entire contents of the cache. To do this, simply use the Clear() method of the MemoryCache class.

app.MapDelete("/cache/clear", (IMemoryCache cache) =>
{
    if (cache is MemoryCache concreteMemoryCache)
        concreteMemoryCache.Clear();

    return Results.Ok("Cache cleared.");
});

Conclusion and Final Considerations

In scenarios where there is a large amount of data and this data must be accessed quickly, adding a caching mechanism can be crucial for the functioning of the application and a good user experience. Furthermore, using the cache prevents excessive access to the source of data, such as a database, resulting in resource savings.

In this post, we saw how to implement in-memory caching in an ASP.NET Core API and how efficient requests are compared to requests without using the cache. Furthermore, we saw how to manipulate the cache by adding, updating and deleting data from the in-memory cache.

When dealing with performance problems, try using the in-memory cache. It may be a simpler and more efficient option for your situation.


This content originally appeared on Telerik Blogs and was authored by Assis Zang


Print Share Comment Cite Upload Translate Updates
APA

Assis Zang | Sciencx (2024-08-13T15:32:03+00:00) ASP.NET Core Basics: Exploring Cache In-Memory. Retrieved from https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/

MLA
" » ASP.NET Core Basics: Exploring Cache In-Memory." Assis Zang | Sciencx - Tuesday August 13, 2024, https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/
HARVARD
Assis Zang | Sciencx Tuesday August 13, 2024 » ASP.NET Core Basics: Exploring Cache In-Memory., viewed ,<https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/>
VANCOUVER
Assis Zang | Sciencx - » ASP.NET Core Basics: Exploring Cache In-Memory. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/
CHICAGO
" » ASP.NET Core Basics: Exploring Cache In-Memory." Assis Zang | Sciencx - Accessed . https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/
IEEE
" » ASP.NET Core Basics: Exploring Cache In-Memory." Assis Zang | Sciencx [Online]. Available: https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/. [Accessed: ]
rf:citation
» ASP.NET Core Basics: Exploring Cache In-Memory | Assis Zang | Sciencx | https://www.scien.cx/2024/08/13/asp-net-core-basics-exploring-cache-in-memory/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.