Caching in ASP.NET Core Web API — Complete Tutorial
1. What is Caching?
Caching means storing frequently used data temporarily so that you don't have to perform the same expensive operation repeatedly.
Without caching:
Client↓API↓Database↓Database Query↓API↓Client
With caching:
Client↓API↓Cache↓Data returned quickly
If the data isn't in cache:
Client↓API↓Cache ❌↓Database↓Cache ← Store result↓Client
Why caching?
Caching can:
- Reduce database queries
- Reduce API response time
- Reduce server load
- Improve scalability
- Reduce network calls to external APIs
- Improve user experience
2. Types of Caching in .NET
The most important types are:
Caching│├── In-Memory Cache│├── Distributed Cache│ ├── Redis│ └── SQL Server│└── HTTP Response Caching
For modern Web API applications, the most important ones to learn are:
-
IMemoryCache -
IDistributedCache - Redis
- Output caching
- Cache-aside pattern
3. In-Memory Caching
In-memory caching stores data directly inside the memory of your API application.
Example:
.NET API Server│├── Application Memory│ ││ ├── Students│ ├── Courses│ └── Categories│└── Database
The cache exists inside the application process.
4. Install/Configure In-Memory Cache
In modern ASP.NET Core, add:
builder.Services.AddMemoryCache();
Example Program.cs:
var builder = WebApplication.CreateBuilder(args);builder.Services.AddControllers();builder.Services.AddMemoryCache();var app = builder.Build();app.MapControllers();app.Run();
5. Basic IMemoryCache Example
Suppose we have:
public class Product{public int Id { get; set; }public string Name { get; set; }public decimal Price { get; set; }}
Controller:
using Microsoft.Extensions.Caching.Memory;[ApiController][Route("api/[controller]")]public class ProductsController : ControllerBase{private readonly IMemoryCache _cache;public ProductsController(IMemoryCache cache){_cache = cache;}[HttpGet("{id}")]public IActionResult GetProduct(int id){string cacheKey = $"product_{id}";if (_cache.TryGetValue(cacheKey, out Product product)){return Ok(product);}// Database callproduct = new Product{Id = id,Name = "Laptop",Price = 50000};_cache.Set(cacheKey, product, TimeSpan.FromMinutes(10));return Ok(product);}}
The first request:
GET /api/products/10Cache ❌↓Database↓Store in Cache↓Response
Second request:
GET /api/products/10Cache ✅↓Response
No database query is required.
6. Cache Expiration
Never blindly keep everything in memory forever.
There are two important expiration types.
Absolute expiration
Data expires after a fixed period.
_cache.Set(cacheKey,product,TimeSpan.FromMinutes(10));
After 10 minutes:
Cache → Expired
7. Sliding Expiration
Sliding expiration resets the expiration time whenever the item is accessed.
var options = new MemoryCacheEntryOptions{SlidingExpiration = TimeSpan.FromMinutes(10)};_cache.Set(cacheKey, product, options);
Suppose:
10:00 → Cache created10:05 → Access10:10 → Access10:15 → Access
The cache continues because it keeps being accessed.
8. Absolute + Sliding Expiration
You can combine them.
var options = new MemoryCacheEntryOptions{SlidingExpiration = TimeSpan.FromMinutes(10),AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)};_cache.Set(cacheKey, product, options);
Meaning:
Maximum lifetime = 1 hourBut if not accessed:expires after 10 minutes
This is often useful for frequently accessed data.
9. GetOrCreateAsync
Instead of manually checking:
if (_cache.TryGetValue(...)){}
you can use:
var product = await _cache.GetOrCreateAsync($"product_{id}",async entry =>{entry.AbsoluteExpirationRelativeToNow =TimeSpan.FromMinutes(10);return await _db.Products.FirstOrDefaultAsync(x => x.Id == id);});
This is cleaner.
10. Real Database Example
Suppose you're using Entity Framework Core.
private readonly AppDbContext _context;private readonly IMemoryCache _cache;public ProductsController(AppDbContext context,IMemoryCache cache){_context = context;_cache = cache;}
API:
[HttpGet]public async Task<IActionResult> GetProducts(){const string cacheKey = "all_products";var products = await _cache.GetOrCreateAsync(cacheKey,async entry =>{entry.AbsoluteExpirationRelativeToNow =TimeSpan.FromMinutes(10);return await _context.Products.AsNoTracking().ToListAsync();});return Ok(products);}
Now:
Request 1↓Cache Miss↓SQL Server↓Store result↓ResponseRequest 2↓Cache Hit↓Response
11. Cache Invalidation
One of the most important caching concepts is:
When database data changes, the cache may contain stale data.
Suppose:
Database:Product Price = ₹50,000Cache:Product Price = ₹50,000
Then you update the database:
Database:Product Price = ₹55,000
But cache still contains:
₹50,000
The API could return incorrect information.
Therefore, after updating the database, remove the cache.
_cache.Remove($"product_{id}");
Example:
[HttpPut("{id}")]public async Task<IActionResult> UpdateProduct(int id,Product product){var existing = await _context.Products.FindAsync(id);if (existing == null)return NotFound();existing.Name = product.Name;existing.Price = product.Price;await _context.SaveChangesAsync();_cache.Remove($"product_{id}");return Ok(existing);}
This is called cache invalidation.
12. Cache Key Design
Cache keys should be unique and predictable.
Bad:
"product"
Better:
$"product_{id}"
For user-specific data:
$"user_{userId}"
For course data:
$"course_{courseId}"
For pagination:
$"products_page_{page}_size_{pageSize}"
For branch-specific data:
$"students_branch_{branchId}"
13. Important Problem with In-Memory Cache
Suppose you have multiple API servers:
Load Balancer│┌─────────┴─────────┐↓ ↓API Server 1 API Server 2│ │Memory MemoryCache Cache
Server 1 has:
product_10
Server 2 doesn't.
If the next request goes to Server 2:
Cache Miss
This happens because each server has its own memory cache.
That's where distributed caching becomes important.
14. Distributed Cache
Distributed cache is shared by multiple application instances.
Architecture:
Load Balancer│┌────────┴────────┐↓ ↓API Server 1 API Server 2│ │└────────┬────────┘↓Redis Cache│↓Database
Both servers access the same cache.
15. Redis
One of the most commonly used distributed caching systems with .NET is Redis.
Typical architecture:
Client↓ASP.NET Core API↓Redis↓SQL Server
Redis is especially useful when:
- Application is running on multiple servers
- Application is containerized
- Kubernetes is being used
- Cloud deployment is used
- High traffic is expected
- Cache needs to be shared
16. IDistributedCache
.NET provides an abstraction:
IDistributedCache
This allows your application to work with distributed caching providers.
You can use:
IDistributedCache
instead of directly coupling your application to Redis.
17. Redis Package
For a Redis implementation, install:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Then configure it.
builder.Services.AddStackExchangeRedisCache(options =>{options.Configuration = "localhost:6379";});
18. Using IDistributedCache
Inject:
private readonly IDistributedCache _cache;public ProductsController(IDistributedCache cache){_cache = cache;}
Store data:
await _cache.SetStringAsync("product_10","Laptop");
Retrieve:
var product = await _cache.GetStringAsync("product_10");
Remove:
await _cache.RemoveAsync("product_10");
19. Redis with JSON Objects
Usually you don't store complex C# objects directly as strings.
Serialize them to JSON.
var product = new Product{Id = 10,Name = "Laptop",Price = 50000};string json = JsonSerializer.Serialize(product);await _cache.SetStringAsync("product_10",json);
Retrieve:
var json = await _cache.GetStringAsync("product_10");if (json != null){var product = JsonSerializer.Deserialize<Product>(json);return Ok(product);}
20. Distributed Cache Expiration
Use:
var options = new DistributedCacheEntryOptions{AbsoluteExpirationRelativeToNow =TimeSpan.FromMinutes(10)};await _cache.SetStringAsync("product_10",json,options);
Sliding expiration:
var options = new DistributedCacheEntryOptions{SlidingExpiration =TimeSpan.FromMinutes(10)};
21. Complete Redis Example
[HttpGet("{id}")]public async Task<IActionResult> GetProduct(int id){string cacheKey = $"product_{id}";// 1. Check Redisvar cachedData =await _cache.GetStringAsync(cacheKey);if (cachedData != null){var cachedProduct =JsonSerializer.Deserialize<Product>(cachedData);return Ok(cachedProduct);}// 2. Databasevar product = await _context.Products.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);if (product == null)return NotFound();// 3. Serializevar json = JsonSerializer.Serialize(product);// 4. Cachevar options = new DistributedCacheEntryOptions{AbsoluteExpirationRelativeToNow =TimeSpan.FromMinutes(10)};await _cache.SetStringAsync(cacheKey,json,options);// 5. Returnreturn Ok(product);}
22. Cache-Aside Pattern
This is one of the most important patterns for interviews.
The application checks the cache first.
Request↓Cache?/ \YES NO↓ ↓Return Database↓Cache↓Return
Pseudo-code:
var data = await cache.Get(key);if (data != null){return data;}data = await database.GetData();await cache.Set(key, data);return data;
This is called the Cache-Aside Pattern.
23. When Should You Use In-Memory Cache?
Use IMemoryCache when:
- You have a single API server
- Data is small
- Data doesn't need to be shared
- Extremely fast access is required
- Cache data is temporary
Examples:
Country listState listCity listCourse categoriesApplication configurationFrequently accessed lookup data
24. When Should You Use Redis?
Use Redis when:
- Multiple API servers exist
- Horizontal scaling is required
- Docker/Kubernetes is used
- Cache must be shared
- High traffic is expected
Example:
Load Balancer│┌─────────────┼─────────────┐↓ ↓ ↓API 1 API 2 API 3│ │ │└─────────────┼─────────────┘↓Redis↓SQL Server
25. In-Memory vs Distributed Cache
| Feature | IMemoryCache | Redis |
|---|---|---|
| Storage | Server RAM | Redis server |
| Speed | Very fast | Very fast |
| Shared between servers | ❌ | ✅ |
| Multiple API instances | Limited | Excellent |
| Persistence | No | Configurable |
| Scalability | Limited | Excellent |
| Setup | Very easy | Requires Redis |
| Best for | Small/local cache | Production distributed systems |
26. Output Caching
Another important caching feature in modern ASP.NET Core is Output Caching.
Output caching caches the HTTP response itself.
Example:
GET /api/productsFirst request↓Controller↓Database↓Response↓Output Cache
Next request:
GET /api/products↓Output Cache↓Response
The controller may not even execute on a cache hit.
27. Configure Output Cache
In Program.cs:
builder.Services.AddOutputCache();
Then:
app.UseOutputCache();
28. Apply Output Cache
[HttpGet][OutputCache(Duration = 60)]public IActionResult GetProducts(){return Ok(products);}
The response is cached for 60 seconds.
29. Output Cache with Tags
You can use policies for more advanced control.
builder.Services.AddOutputCache(options =>{options.AddPolicy("Products", policy =>{policy.Expire(TimeSpan.FromMinutes(5));});});
Then:
[OutputCache(PolicyName = "Products")][HttpGet]public IActionResult GetProducts(){return Ok(products);}
30. Response Caching vs Output Caching
These are often confused.
Response caching
Primarily relies on HTTP caching semantics and headers.
Output caching
Allows the server to cache generated responses and reuse them.
For modern ASP.NET Core APIs, Output Caching is an important feature to understand.
31. What Data Should You Cache?
Good candidates:
✓ Product catalog✓ Course list✓ Category list✓ Country/state/city✓ Frequently accessed reports✓ Configuration✓ Public API responses✓ Expensive database queries✓ External API responses
Avoid caching:
✗ Frequently changing data✗ Highly sensitive information✗ User-specific data without proper keys✗ Large objects unnecessarily✗ Data requiring real-time accuracy
32. Don't Cache Everything
Caching isn't automatically good.
Suppose:
Database query = 2 msCache serialization = 5 msRedis network = 3 ms
Caching that query may actually make things slower.
You should cache expensive and frequently requested data.
33. Cache Performance Strategy
A good strategy is:
Request↓L1 Cache(IMemoryCache)↓L2 Cache(Redis)↓Database
This is called a multi-level caching approach.
Example:
Client↓API↓Memory Cache↓ missRedis↓ missSQL Server
This can provide excellent performance for high-scale applications.
34. Database Performance Tips
Caching is only one part of API performance.
You should also optimize your database queries.
Instead of:
var students = await _context.Students.ToListAsync();
if you only need selected fields:
var students = await _context.Students.AsNoTracking().Select(x => new{x.Id,x.Name,x.Email}).ToListAsync();
This reduces unnecessary data retrieval.
35. Use AsNoTracking
For read-only queries:
var students = await _context.Students.AsNoTracking().ToListAsync();
Why?
EF Core doesn't need to track entities that you aren't modifying.
This can improve read performance.
36. Pagination
Never return 100,000 records unnecessarily.
Bad:
var students = await _context.Students.ToListAsync();
Better:
var students = await _context.Students.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
Example:
Page = 1Size = 20Records:1 - 20
Next:
Page = 2Size = 20Records:21 - 40
37. Async Programming
Use:
asyncawait
Example:
var students =await _context.Students.AsNoTracking().ToListAsync();
Avoid blocking:
var students =_context.Students.ToList();
and especially:
.Result.Wait()
in ASP.NET Core application code.
38. Avoid N+1 Queries
Bad pattern:
foreach (var student in students){var course = await _context.Courses.FirstOrDefaultAsync(x => x.Id == student.CourseId);}
This can produce:
1 query for students+100 queries for courses=101 queries
Instead, use appropriate projection or joins.
For example:
var students = await _context.Students.Select(s => new{s.Id,s.Name,CourseName = s.Course.Name}).ToListAsync();
39. Database Indexing
If you frequently query:
_context.Students.Where(x => x.Email == email)
make sure the database has an appropriate index.
For example:
CREATE INDEX IX_Students_EmailON Students(Email);
Indexes can dramatically improve lookup performance.
40. Avoid Returning Huge JSON
Instead of:
Student├── 50 properties├── Courses├── Attendance├── Fees├── Payments├── Documents└── History
return only what the endpoint needs.
.Select(x => new StudentDto{Id = x.Id,Name = x.Name,Email = x.Email})
41. Compression
For large API responses, compression can reduce network traffic.
ASP.NET Core supports response compression.
builder.Services.AddResponseCompression();
Then:
app.UseResponseCompression();
This is useful when responses are large, but you should measure the CPU/network tradeoff.
42. Avoid Unnecessary Serialization
Serialization can become expensive for large objects.
Use DTOs:
public class StudentDto{public int Id { get; set; }public string Name { get; set; }public string Email { get; set; }}
Instead of exposing large EF entities directly.
43. API Performance Architecture
A production API can look like this:
Client│↓Load Balancer│┌───────────┼───────────┐↓ ↓ ↓API 1 API 2 API 3│ │ │└───────────┼───────────┘↓Memory Cache↓Redis↓EF Core↓SQL Server
With:
Pagination+AsNoTracking+Indexes+DTO Projection+Async+Caching+Compression
you can significantly improve API performance.
44. Practical Project Example
For your type of application, imagine a Training Institute Management API.
You might have:
StudentsCoursesFacultyBranchesBatchesAttendanceFees
Cache courses
GET /api/courses
Cache for:
30 minutes
Because courses don't change frequently.
Cache branches
GET /api/branches
Cache for:
1 hour
Don't cache attendance blindly
GET /api/attendance/today
Because attendance changes frequently.
Don't cache payment status for a long time
GET /api/student/fees/123
Because financial information needs relatively fresh data.
45. Recommended Cache Times
There is no universal value, but a starting point could be:
| Data | Suggested TTL |
|---|---|
| Countries | 24 hours |
| States | 24 hours |
| Courses | 30–60 minutes |
| Categories | 30–60 minutes |
| Branches | 30 minutes |
| Faculty list | 5–15 minutes |
| Student list | 1–5 minutes |
| Dashboard | 30 sec–5 min |
| Attendance | Very short / no cache |
| Payment status | Short / carefully invalidated |
These should be adjusted based on how frequently the underlying data changes.
46. Cache Stampede Problem
Imagine a cache expires at exactly:
10:00:00
100 requests arrive simultaneously.
All see:
Cache MISS
Then all 100 hit SQL Server.
100 requests↓100 database queries
This is called a cache stampede or thundering herd problem.
For high-traffic systems, you need strategies such as:
- Locking/single-flight
- Staggered expiration
- Background refresh
- Distributed locking
- Refresh-ahead caching
47. Cache Security
Never blindly cache sensitive data.
Be particularly careful with:
PasswordsAccess tokensRefresh tokensPayment informationPersonal sensitive dataAuthorization decisions
If caching user-specific information, include the user/tenant identity in the key.
Bad:
0 Comments
POST Answer of Questions and ASK to Doubt