A Quick view of Cached Repository Pattern
Khalid BOURZAYQ
Posted on January 31, 2022
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();
}
}
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);
}
}
Time to inject dependencies and lunch our app:
Builder.Services.AddScoped<IReadOnlyRepository<Customer>, CustomerRepository>();
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
},
....
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();
});
}
}
The next setp is to change the injected dependencies :
Builder.Services.AddScoped<IReadOnlyRepository<Customer>, CachedCustomerRepository>();
Builder.Services.AddScoped<CustomerRepository>();
Builder.Services.AddSingleton<IMemoryCache, MemoryCache>();
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
},
....
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
},
....
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.
Posted on January 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.