Implementasi In-Memory Cache pada ASP.NET Core

4 Mei 2024

Penggunaan caching merupakan salah satu cara untuk meningkatkan performa aplikasi kita. Caching sendiri adalah proses untuk menyimpan data dari aplikasi kita dengan tujuan agar data tersebut dapat ditampilkan atau diakses dengan mengurangi beban pada proses yang dibutuhkan ketika mengakses data. Efeknya, data yang dibutuhkan dapat diakses secara lebih cepat. Proses ini sangat efektif untuk data yang jarang berubah ataupun data yang ‘mahal’ prosesnya ketika diakses.

Saat ini, cache sudah sangat lazim digunakan. Cache dapat diterapkan dengan cara menyimpan cache pada memori (in-memory cache), menggunakan layanan caching seperti AWS Elasticache, atau dengan metode-metode lain.

Pada artikel ini, kita akan menggunakan cache dengan konsep in-memory cache untuk aplikasi ASP.NET Core. Framework .NET sendiri sudah mempunyai IMemoryCache yang bisa kita gunakan untuk membuat caching dengan penggunaan yang sangat mudah. Hal ini membantu kita untuk mengoptimalkan performa aplikasi kita dengan lebih efisien.

Penggunaan In-Memory Caching

Saat menggunakan caching, kawan-kawan harus mempertimbangkan poin-poin ini.

  • Pada aplikasi kita, harus ada opsi untuk mengakses data (fetch data) sehingga tidak bergantung hanya pada cache.
  • Perlu diingat, karena cache menggunakan memori, maka kawan-kawan harus mempertimbangkan kapan cache kadaluarsa (cache expiration), membatasi ukuran cache, dan penggunaan cache key yang efisien.

Selanjutnya, mari kita langsung implementasi caching. Contoh yang akan kita gunakan adalah default ASP.NET Core Web API project yaitu Weather Forecast.

dotnet new webapi -o in-memory-caching-weather-forecast

Lalu masuk ke direktori `in-memory-caching-weather-forecast’

cd in-memory-caching-weather-forecast

Selanjutnya, buka project ini melalui IDE favorit kawan-kawan. Buka file Program.cs dan tambahkan informasi mengenai CreatedAt dengan tipe data Datetime pada record WeatherForecast sehingga menjadi seperti ini.

...

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary, DateTime CreatedAt)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Kemudian ubah API /weatherforecast dengan menambahkan informasi waktu saat ini dengan DateTime.now. Kode ini akan menghasilkan data berupa ramalan cuaca secara acak sesuai informasi pada variable summaries.

...

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)],
            DateTime.Now // Penambahan informasi meengenai waktu
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

...

Setelah itu, mari kita tambahkan IMemoryCache pada Program.cs dengan singleton seperti ini.

using Microsoft.Extensions.Caching.Memory; // menambahkan IMemoryCache

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IMemoryCache, MemoryCache>(); // menambahkan singleton

...

Langkah selanjutnya, kita akan menggunakan TryGetValue untuk cek apakah info ramalan cuaca sudah ada cache. Apabila belum ada cache, maka akan dihasilkan data ramalan cuaca dan membuat cache dari data tersebut.

app.MapGet("/weatherforecast", (IMemoryCache memoryCache) =>
    {
        WeatherForecast[] forecast = []; // variable kosong

        if (!memoryCache.TryGetValue("forecastData", out WeatherForecast[] cacheValue)) // cek cache
        {
            // generate data
            cacheValue = Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)],
                DateTime.Now
            ))
            .ToArray();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromSeconds(15));

            memoryCache.Set("forecastData", cacheValue, cacheEntryOptions);
        }

        forecast = cacheValue.ToArray();

        return forecast;
    }
)
.WithName("GetWeatherForecast")
.WithOpenApi();

Pada kode di atas, "forecastData" menjadi cache key. SetSlidingExpiration berfungsi untuk menghapus cache apabila tidak diakses dalam jangka waktu 15 detik. Apabila dikases dalam janghka waktu 15 detik, maka cache akan diperpanjang selama 15 detik lagi.

Mari kita tes aplikasi ini. Pada root folder ‘in-memory-caching-weather-forecast’, jalankan ini.

dotnet run

Lalu buka aplikasi melalui browser. Kemudian teman-teman bisa mengunjungi rute /weatherforecast seperti ini http://localhost:5246/weatherforecast. Maka akan muncul JSON seperti ini. Apabila teman-teman mengakses data ini dalam kurun waktu 15 detik, maka data yang muncul akan selalu sama karena data ini tersimpan sebagai cache. Berikut contoh datanya.

[
  {
    "date": "2024-05-14",
    "temperatureC": 37,
    "summary": "Chilly",
    "createdAt": "2024-05-13T00:01:24.222915+07:00",
    "temperatureF": 98
  },
  {
    "date": "2024-05-15",
    "temperatureC": 23,
    "summary": "Hot",
    "createdAt": "2024-05-13T00:01:24.22302+07:00",
    "temperatureF": 73
  },
  {
    "date": "2024-05-16",
    "temperatureC": 51,
    "summary": "Balmy",
    "createdAt": "2024-05-13T00:01:24.2230203+07:00",
    "temperatureF": 123
  },
  {
    "date": "2024-05-17",
    "temperatureC": 32,
    "summary": "Hot",
    "createdAt": "2024-05-13T00:01:24.2230205+07:00",
    "temperatureF": 89
  },
  {
    "date": "2024-05-18",
    "temperatureC": 40,
    "summary": "Chilly",
    "createdAt": "2024-05-13T00:01:24.2230215+07:00",
    "temperatureF": 103
  }
]

Apabila teman-teman mengakses kemabli rute /weatherforecast setelah 15 detik, maka datanya akan berubah karena dihasilkan data baru dari aplikasi kita. Contohnya seperti ini.


  {
    "date": "2024-05-14",
    "temperatureC": 39,
    "summary": "Hot",
    "createdAt": "2024-05-13T00:02:35.0194849+07:00",
    "temperatureF": 102
  },
  {
    "date": "2024-05-15",
    "temperatureC": 47,
    "summary": "Chilly",
    "createdAt": "2024-05-13T00:02:35.0194861+07:00",
    "temperatureF": 116
  },
  {
    "date": "2024-05-16",
    "temperatureC": 36,
    "summary": "Chilly",
    "createdAt": "2024-05-13T00:02:35.0194911+07:00",
    "temperatureF": 96
  },
  {
    "date": "2024-05-17",
    "temperatureC": 53,
    "summary": "Chilly",
    "createdAt": "2024-05-13T00:02:35.0194918+07:00",
    "temperatureF": 127
  },
  {
    "date": "2024-05-18",
    "temperatureC": 32,
    "summary": "Scorching",
    "createdAt": "2024-05-13T00:02:35.0194923+07:00",
    "temperatureF": 89
  }
]

Jangka waktu kadaluarsa cache ini dapat kita ubah menggunakan method Set tanpa harus menggunakan MemoryCacheEntryOptions seperti kode sebelumnya. Apabila menggunakan metode ini, maka cache akan kadaluarsa dan terhapus sesuai durasi yang digunakan walaupun cache diakses berkali-kali. Mari kita set agar cache terhapus setelah 15 detik.

memoryCache.Set("forecastData", cacheValue, TimeSpan.FromSeconds(15));

Bila teman-teman mengunjungi rute ‘/weatherforecast’ maka akan muncul data yang akan selalu berubah setiap 15 detik karena cache akan selalu kadaluarsa setiap 15 detik.

Selanjuntya, cache yang sudah disimpan didalam memori ini bisa kita akses menggunakan cache key yaitu "forecastData". Kita bisa menggunakan Get method untuk mengaksesnya. Okay, mari kita buat rute lain yaitu /cacheforecast untuk mengambil data dari cache.

app.MapGet("/cacheforecast", (IMemoryCache memoryCache) =>
    {
        var cacheEntry = memoryCache.Get<WeatherForecast[]?>("forecastData");
        return cacheEntry!.ToList();
    }
)
.WithName("GetCacheForecast")
.WithOpenApi();

Lalu teman-teman bisa membuka rute ‘/weatherforecast’ terlebih dahulu, lalu membuka rute ‘/cacheforecast’. Data yang ditampilkan akan sama karena keduanya mengambil data dari cache yang sama.

Kita juga bisa meenggunakan GetOrCreate dan GetOrCreateAsync untuk menambahkan fungsi apabila cache belum ada ketika diakses, maka dapat dibuat cache dengan data yang dibutuhkan. Kita modifikasi rute /cacheforecast dengan menggunakan GetOrCreate.

...

// mengambil data dari cache
app.MapGet("/cacheforecast", (IMemoryCache memoryCache) =>
    {
        // Mengakses Cache
        // var cacheEntry = memoryCache.Get<WeatherForecast[]?>("forecastData");

        // Mengakses Cache atau Membuat Cache
        var cacheData = Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)],
                DateTime.Now
            ))
            .ToArray();

        var cacheEntry = memoryCache.GetOrCreate(
            "forecastData",
            cacheEntry =>
            {
                cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(15);
                return cacheData;
            });
        return cacheEntry!.ToList();
    }
)
.WithName("GetCacheForecast")
.WithOpenApi();

...

Ketika mengunjungi rute /cacheforecast maka akan dibuat data mengenai ramalan cuaca apabila tidak cache tentang data ini.

Selanjutnya, kita akan melakukan implementasi untuk menghapus cache. Cache bisa terhapus apabila sudah melebihi masa kadaluarsanya. Namun, kita bisa menggunakan method Remove untuk menghapus cache. Mari kita modifikasi rute /cacheforecast dengan menambahkan Remove. Ketike rute ini dipanggil, kita akan hapus cache forecastData dan memberikan data baru untuk cache.


app.MapGet("/cacheforecast", (IMemoryCache memoryCache) =>
    {
        // Mengakses Cache
        // var cacheEntry = memoryCache.Get<WeatherForecast[]?>("forecastData");

        // Mengakses Cache atau Membuat Cache
        var cacheData = Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)],
                DateTime.Now
            ))
            .ToArray();

        memoryCache.Remove("forecastData"); // menghapus cache dengan key forecastData

        var cacheEntry = memoryCache.GetOrCreate(
            "forecastData",
            cacheEntry =>
            {
                cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(15);
                return cacheData;
            });
        return cacheEntry!.ToList();
    }
)
.WithName("GetCacheForecast")
.WithOpenApi();

Sebagai perbandingan, ketika rute /weatherforecast diakses lebih dulu sebelum /cacheforest, maka kedua rute tersebut akan memberikan data yang berbeda. Setelah teman-teman mengakses /cacheforecast dan kembali mengakses rute /weatherforecast maka akan muncul data yang sama karena sudah ada cache yang dibuat dengan data saat ada kunjungan ke rute cacheforecast.

Best Practices

Sebagai penerapan caching yang mengikuti best practices, teman-teman bisa menggunakan strategi kombinasi Sliding Expiration dan Absolute Expiration serta membatasi size dari cache.

Kombinasi Sliding Expiration dan Absolute Expiration dapat kita terapkan pada rute cacheforecast dengan menambahkan SetAbsoluteExpiration untuk menghindari resiko cache yang tidak pernah kadaluarsa karena sering diakses dan masih menggunakan Sliding Expiration.

...
app.MapGet("/cacheforecast", (IMemoryCache memoryCache) =>
    {
        // Mengakses Cache
        // var cacheEntry = memoryCache.Get<WeatherForecast[]?>("forecastData");

        // Mengakses Cache atau Membuat Cache
        var cacheData = Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)],
                DateTime.Now
            ))
            .ToArray();

        memoryCache.Remove("forecastData");

        var cacheEntry = memoryCache.GetOrCreate(
            "forecastData",
            cacheEntry =>
            {
                cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(15);
                cacheEntry.SetAbsoluteExpiration(TimeSpan.FromSeconds(60)); // best practice
                return cacheData;
            });
        return cacheEntry!.ToList();
    }
)
.WithName("GetCacheForecast")
.WithOpenApi();
...

Maka cache dapat diakses berkali-kali dengan maksimal satu menit. Apabila lebih dari satu menit, cache tersebut dibuat kadaluarsa.

Selanjutnya, kita juga bisa menggunakan size untuk membatasi ukuran cache. Rute /cacheforecast dapat kita tambahkan size dengan method SetSize.

...
app.MapGet("/cacheforecast", (IMemoryCache memoryCache) =>
    {
        // Mengakses Cache
        // var cacheEntry = memoryCache.Get<WeatherForecast[]?>("forecastData");

        // Mengakses Cache atau Membuat Cache
        var cacheData = Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)],
                DateTime.Now
            ))
            .ToArray();

        memoryCache.Remove("forecastData");

        var cacheEntry = memoryCache.GetOrCreate(
            "forecastData",
            cacheEntry =>
            {
                cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(15);
                cacheEntry.SetAbsoluteExpiration(TimeSpan.FromSeconds(60)); // best practice
                cacheEntry.SetSize(2); // best practice
                return cacheData;
            });
        return cacheEntry!.ToList();
    }
)
.WithName("GetCacheForecast")
.WithOpenApi();
...

SetSize dengan angka 2 ini berarti cache dapat menyimpan sebanyak 2 byte.

Catatan

Ada hal yang menarik ketika kita menggunakan caching. Apabila cache melebihi limit, maka cache tidak akan menghapus data cache yang lama untuk data yang baru. Proses yang terjadi adalah data cahce yang baru akan diabaikan dan tidak ada error yang muncul. Oleh karena itu, teman-teman harus hati-hati ketika ingin menerapkan caching.

Okay, sangat mudah bukan menggunakan In-Memory Cache di ASP.NET Core. Caching merupakan metode yang bagus untuk meningkatkan performa aplikasi kita. Teman-teman bisa mengikuti langkah-langkah caching pada artikel ini untuk aplikasi teman-teman. Semoga artikel ini bermanfaat buat teman-teman semua.

Happy coding!

Source Code Tutorial - Github DevKage
Suka konten ini? ❤️ Dukung DevKage melalui Saweria.