Ad Code

✨🎆 JOIN MERN, JAVA, PYTHON, AI, DEVOPS, SALESFORCE Courses 🎆✨

Get 100% Placement Oriented Program CLICK to new more info click

Caching in ASP.NET Core Web API — Complete Tutorial

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:

  1. IMemoryCache
  2. IDistributedCache
  3. Redis
  4. Output caching
  5. 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 call
product = new Product
{
Id = id,
Name = "Laptop",
Price = 50000
};

_cache.Set(cacheKey, product, TimeSpan.FromMinutes(10));

return Ok(product);
}
}

The first request:

GET /api/products/10

Cache ❌
Database
Store in Cache
Response

Second request:

GET /api/products/10

Cache ✅
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 created
10:05 → Access
10:10 → Access
10: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 hour

But 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
Response

Request 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,000

Cache:

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 Memory
Cache 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 Redis
var cachedData =
await _cache.GetStringAsync(cacheKey);

if (cachedData != null)
{
var cachedProduct =
JsonSerializer.Deserialize<Product>(cachedData);

return Ok(cachedProduct);
}

// 2. Database
var product = await _context.Products
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);

if (product == null)
return NotFound();

// 3. Serialize
var json = JsonSerializer.Serialize(product);

// 4. Cache
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(10)
};

await _cache.SetStringAsync(
cacheKey,
json,
options
);

// 5. Return
return 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 list
State list
City list
Course categories
Application configuration
Frequently 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

FeatureIMemoryCacheRedis
StorageServer RAMRedis server
SpeedVery fastVery fast
Shared between servers
Multiple API instancesLimitedExcellent
PersistenceNoConfigurable
ScalabilityLimitedExcellent
SetupVery easyRequires Redis
Best forSmall/local cacheProduction 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/products

First 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 ms
Cache serialization = 5 ms
Redis 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
↓ miss
Redis
↓ miss
SQL 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 = 1
Size = 20

Records:
1 - 20

Next:

Page = 2
Size = 20

Records:
21 - 40

37. Async Programming

Use:

async
await

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_Email
ON 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:

Students
Courses
Faculty
Branches
Batches
Attendance
Fees

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:

DataSuggested TTL
Countries24 hours
States24 hours
Courses30–60 minutes
Categories30–60 minutes
Branches30 minutes
Faculty list5–15 minutes
Student list1–5 minutes
Dashboard30 sec–5 min
AttendanceVery short / no cache
Payment statusShort / 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:

Passwords
Access tokens
Refresh tokens
Payment information
Personal sensitive data
Authorization decisions

If caching user-specific information, include the user/tenant identity in the key.

Bad:

 

Post a Comment

0 Comments