Core API Response Caching
The BookStore API reads book data from the database on every single request. For a list of books that rarely changes, this means hundreds of identical database queries per minute under high traffic. Response caching stores the result of a request and returns the stored result for subsequent identical requests — dramatically reducing database load and improving response speed.
Types of Caching
| Type | Where Data Is Stored | Best For |
|---|---|---|
| In-Memory Cache | Server's RAM | Single server, fast access |
| Distributed Cache (Redis) | External Redis server | Multiple servers, shared cache |
| HTTP Response Cache | Client browser / CDN | Static or rarely changing public data |
This topic covers In-Memory Caching — the simplest approach for the BookStore API.
Step 1 – Register In-Memory Cache
// Program.cs
builder.Services.AddMemoryCache();
Step 2 – Use IMemoryCache in BookService
// Services/BookService.cs
using Microsoft.Extensions.Caching.Memory;
public class BookService : IBookService
{
private readonly IBookRepository _bookRepository;
private readonly IMemoryCache _cache;
private readonly ILogger<BookService> _logger;
// Cache key constants — avoids typos
private const string AllBooksCacheKey = "AllBooks";
private const string BookByIdCacheKeyPrefix = "Book_";
public BookService(
IBookRepository bookRepository,
IMemoryCache cache,
ILogger<BookService> logger)
{
_bookRepository = bookRepository;
_cache = cache;
_logger = logger;
}
public async Task<List<Book>> GetAllAsync()
{
// Try to get from cache first
if (_cache.TryGetValue(AllBooksCacheKey, out List<Book>? cachedBooks) && cachedBooks != null)
{
_logger.LogInformation("Books served from cache.");
return cachedBooks;
}
// Cache miss — fetch from database
_logger.LogInformation("Cache miss. Fetching books from database.");
var books = await _bookRepository.GetAllAsync();
// Store in cache for 5 minutes
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
.SetSlidingExpiration(TimeSpan.FromMinutes(2));
_cache.Set(AllBooksCacheKey, books, cacheOptions);
return books;
}
public async Task<Book?> GetByIdAsync(int id)
{
string cacheKey = $"{BookByIdCacheKeyPrefix}{id}";
if (_cache.TryGetValue(cacheKey, out Book? cachedBook) && cachedBook != null)
{
_logger.LogInformation("Book {Id} served from cache.", id);
return cachedBook;
}
var book = await _bookRepository.GetByIdAsync(id);
if (book != null)
{
_cache.Set(cacheKey, book, TimeSpan.FromMinutes(5));
}
return book;
}
public async Task<Book> CreateAsync(Book book)
{
book.CreatedDate = DateTime.UtcNow;
await _bookRepository.AddAsync(book);
await _bookRepository.SaveAsync();
// Invalidate the all-books cache — it is now stale
_cache.Remove(AllBooksCacheKey);
_logger.LogInformation("Cache invalidated after book creation.");
return book;
}
public async Task<bool> UpdateAsync(int id, Book updatedBook)
{
var book = await _bookRepository.GetByIdAsync(id);
if (book == null) return false;
book.Title = updatedBook.Title;
book.Author = updatedBook.Author;
book.Price = updatedBook.Price;
book.Category = updatedBook.Category;
book.IsAvailable = updatedBook.IsAvailable;
await _bookRepository.UpdateAsync(book);
await _bookRepository.SaveAsync();
// Invalidate both caches
_cache.Remove(AllBooksCacheKey);
_cache.Remove($"{BookByIdCacheKeyPrefix}{id}");
return true;
}
public async Task<bool> DeleteAsync(int id)
{
var book = await _bookRepository.GetByIdAsync(id);
if (book == null) return false;
await _bookRepository.DeleteAsync(book);
await _bookRepository.SaveAsync();
// Invalidate both caches
_cache.Remove(AllBooksCacheKey);
_cache.Remove($"{BookByIdCacheKeyPrefix}{id}");
return true;
}
}
Cache Options Explained
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)) // Always expires after 5 min
.SetSlidingExpiration(TimeSpan.FromMinutes(2)) // Expires 2 min after last access
.SetPriority(CacheItemPriority.Normal); // Low/Normal/High/NeverRemove
| Option | Meaning | BookStore Use |
|---|---|---|
| AbsoluteExpiration | Cache entry expires at a fixed time, regardless of access | Book list expires after 5 minutes |
| SlidingExpiration | Cache entry resets its timer on each access | Individual book cache resets if frequently accessed |
| Priority | When memory is low, low-priority items are removed first | Book list is Normal priority |
Cache Flow Diagram
Request: GET /api/books
Check Cache: "AllBooks" key exists?
|
├── YES (cache hit)
│ → Return cached book list
│ ⏱ ~1ms response time
│
└── NO (cache miss)
→ Query database (SELECT * FROM Books)
→ Store result in cache with 5-min expiry
→ Return book list
⏱ ~50-200ms response time
Next request within 5 minutes:
→ Cache hit → ~1ms response time ✓
HTTP Response Caching Headers
In addition to server-side caching, HTTP headers can instruct the client's browser or a CDN to cache the response:
// Program.cs
builder.Services.AddResponseCaching();
// In middleware pipeline
app.UseResponseCaching();
// In the controller action
[HttpGet]
[AllowAnonymous]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)]
public async Task<IActionResult> GetAll()
{
var books = await _bookService.GetAllAsync();
return Ok(_mapper.Map<List<BookResponseDto>>(books));
}
This adds the following header to the HTTP response:
Cache-Control: public, max-age=60
The browser caches the response for 60 seconds. The same request within that time returns the cached response without hitting the server at all.
Cache Invalidation Strategy for BookStore API
| Operation | Cache Action |
|---|---|
| GET all books | Read from cache → return. If miss, query DB then store |
| GET book by Id | Read from cache → return. If miss, query DB then store |
| POST (create book) | Remove AllBooks cache — list is stale |
| PUT (update book) | Remove AllBooks and Book_{id} cache |
| DELETE book | Remove AllBooks and Book_{id} cache |
Key Points
- Caching stores the result of expensive operations (database queries) in memory and returns the stored result for subsequent identical requests.
IMemoryCachestores data in the server's RAM — fast, but not shared across multiple server instances.- Cache must be invalidated (removed) whenever the underlying data changes — after create, update, or delete operations.
- Absolute expiration removes the cache entry after a fixed duration; sliding expiration resets the timer on each access.
- HTTP response caching headers (
Cache-Control) tell the browser or CDN to cache responses on the client side.
