DEV Community

Khalid BOURZAYQ
Khalid BOURZAYQ

Posted on • Updated on

A Quick view of Cached Repository Pattern

In this article, we will see how to use the Cached Repository pattern with asp.net core web api and entity framework core.

The idea

In this exemple we will create an aspnet core webapi that fetches a list of cutomers stored in the database and the result will be a Data transfert object (DTO) that contains the list of customers and the time taken to fetch the data.

The database will be initialized with 10000 record for testing.

You can find the full source code of this sample in the link below:
https://github.com/kbourzayq/CachedRepository

The Repository Class before Applying cache

The repository class looks like :

public class CustomerRepository : IReadOnlyRepository<Customer>
{
    private readonly AppDbContext _context;

    public CustomerRepository(AppDbContext context)
    {
        _context = context;
    }

    public Customer GetById(int id)
    {
        return _context.Cutomers.First(x => x.Id == id);
    }

    public List<Customer> List()
    {
        return _context.Cutomers.Take(1000).ToList();
    }
}
Enter fullscreen mode Exit fullscreen mode

The Api Controller class :

[Route("api/[controller]")]
[ApiController]
public class CustomerController : ControllerBase
{
    private readonly IReadOnlyRepository<Customer> _repository;

    public CustomerController(IReadOnlyRepository<Customer> repository)
    {
        _repository = repository;
    }
    [HttpGet]
    public ActionResult<CustomerListDto> GetAll()
    {
        var timer = Stopwatch.StartNew();
        var customers = _repository.List();
        timer.Stop();
        CustomerListDto customerList = new CustomerListDto { 
            Customers = customers, 
            ElapsedTimeMilliseconds = timer.ElapsedMilliseconds
        };
        return Ok(customerList);
    }
}
Enter fullscreen mode Exit fullscreen mode

Time to inject dependencies and lunch our app:


Builder.Services.AddScoped<IReadOnlyRepository<Customer>, CustomerRepository>();

Enter fullscreen mode Exit fullscreen mode

As you can see the loading of 1000 records takes some time...

{
  "elapsedTimeMilliseconds": 1226,
  "customers": [
    {
      "name": "Customer 0",
      "lastName": "Customer Lastname 0",
      "address": "Address 0",
      "id": 1
    },
    {
      "name": "Customer 1",
      "lastName": "Customer Lastname 1",
      "address": "Address 1",
      "id": 2
    },
    {
      "name": "Customer 2",
      "lastName": "Customer Lastname 2",
      "address": "Address 2",
      "id": 3
    },
    ....
Enter fullscreen mode Exit fullscreen mode

Let's apply cache

In this example, we want to add caching behavior without modifying the existing code.
In other words, we should add caching to the application without touching any code in the repository implementation shown above or the webapi that uses it.
See: Open closed principle.
To do that, we will use the Decorator pattern.
The Decorator pattern is used to add some behaviors to an existing type without affecting the behavior of other types.

The Cached Repository class looks like the code below :

public class CachedCustomerRepository : IReadOnlyRepository<Customer>
{
    private readonly CustomerRepository _repository;
    private readonly IMemoryCache _cache;
    private const string CustomersCacheKey = "Sample::Customers";
    private MemoryCacheEntryOptions cacheOptions;


    public CachedCustomerRepository(CustomerRepository repository,
        IMemoryCache cache)
    {
        _repository = repository;
        _cache = cache;
        cacheOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(relative: TimeSpan.FromSeconds(20));
    }

    public Customer GetById(int id)
    {
        string key = CustomersCacheKey + "-" + id;

        return _cache.GetOrCreate(key, entry =>
        {
            entry.SetOptions(cacheOptions);
            return _repository.GetById(id);
        });
    }

    public List<Customer> List()
    {
        return _cache.GetOrCreate(CustomersCacheKey, entry =>
        {
            entry.SetOptions(cacheOptions);
            return _repository.List();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The next setp is to change the injected dependencies :

Builder.Services.AddScoped<IReadOnlyRepository<Customer>, CachedCustomerRepository>();
Builder.Services.AddScoped<CustomerRepository>();
Builder.Services.AddSingleton<IMemoryCache, MemoryCache>();
Enter fullscreen mode Exit fullscreen mode

Now, after implementing our cache, we will launch our application and see what it will give us

First call :

{
  "elapsedTimeMilliseconds": 1224,
  "customers": [
    {
      "name": "Customer 0",
      "lastName": "Customer Lastname 0",
      "address": "Address 0",
      "id": 1
    },
    {
      "name": "Customer 1",
      "lastName": "Customer Lastname 1",
      "address": "Address 1",
      "id": 2
    },
    {
      "name": "Customer 2",
      "lastName": "Customer Lastname 2",
      "address": "Address 2",
      "id": 3
    },
    ....
Enter fullscreen mode Exit fullscreen mode

Second call :

{
  "elapsedTimeMilliseconds": 0,
  "customers": [
    {
      "name": "Customer 0",
      "lastName": "Customer Lastname 0",
      "address": "Address 0",
      "id": 1
    },
    {
      "name": "Customer 1",
      "lastName": "Customer Lastname 1",
      "address": "Address 1",
      "id": 2
    },
    {
      "name": "Customer 2",
      "lastName": "Customer Lastname 2",
      "address": "Address 2",
      "id": 3
    },
    ....
Enter fullscreen mode Exit fullscreen mode

As you can see, loading a large number of records takes some time (between 150 to 1224 ms on my machine) on the first load, but then drops to 0 ms for subsequent calls because we put the data in our cache.

In our app example, we set a cache expiration value of 20 seconds, so once the 20 seconds are up, we will reload the data from the database and cache it for the next 20 seconds.

Conclusion

In the example we just saw, it was a small application that tries to retrieve a large number of data that we are going to put in a memory cache.
In the case of distributed applications you will have to use a distributed cache such as Redis or memcached.
You can find in the link below a very small application that uses Redis :
https://github.com/kbourzayq/RedisDemo

The idea of this example is to explain how we can implement the Repository pattern by caching the data to have high performance in the case where we have data that has a very minimal change frequency.
I hope this article was clear for you and do not hesitate to leave your comments if you see that it needs improvements.

See you next post in the next few days.

Top comments (0)