- Output Caching là gì và khác Response Caching ra sao?
- Làm sao để cấu hình output caching trong .NET 7+?
- Cache policies hoạt động như thế nào?
- Khi nào nên dùng output caching?
- Cached tagged responses là gì?
┌─────────────────────────────────────────────────────────────────┐
│ CACHING COMPARISON │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Output Caching (.NET 7+): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Server │───▶│ Cache │ │
│ │ Request │ │ (App) │ │ (Mem) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Cache trên server │
│ - Không gửi cache headers cho client │
│ - Phù hợp cho server-side caching │
│ │
│ Response Caching: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Server │───▶│ Client │ │
│ │ Request │ │ (App) │ │ Cache │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Cache trên client/proxy │
│ - Gửi cache headers (Cache-Control) │
│ - Phù hợp cho CDN/browser caching │
│ │
└─────────────────────────────────────────────────────────────────┘
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add output caching
builder.Services.AddOutputCache();
var app = builder.Build();
// Minimal APIs: Enable caching for endpoints
app.MapGet("/api/products", () => GetProducts())
.CacheOutput();
app.Run();
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add output caching
builder.Services.AddOutputCache();
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
// IMPORTANT: Phải đặt sau UseRouting và trước MapControllers
app.UseRouting();
app.UseOutputCache();
app.MapControllers();
app.Run();
builder.Services.AddOutputCache(options =>
{
// Default policy - cache 60 seconds
options.AddBasePolicy(builder => builder.Cache());
// Custom policy - cache 5 minutes
options.AddPolicy("ShortCache", builder =>
builder.Cache().Expire(TimeSpan.FromMinutes(5)));
// Custom policy - no cache
options.AddPolicy("NoCache", builder => builder.NoCache());
// Custom policy - vary by header
options.AddPolicy("VaryByHeader", builder =>
builder.Cache().VaryByHeader("Accept-Language"));
});
// Cache với default policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput();
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[OutputCache(Duration = 60)]
public async Task<IActionResult> GetProducts([FromServices] AppDbContext db)
{
return Ok(await db.Products.ToListAsync());
}
}
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[OutputCache(PolicyName = "ShortCache")]
public async Task<IActionResult> GetProducts([FromServices] AppDbContext db)
{
return Ok(await db.Products.ToListAsync());
}
}
### Vary By Query
```csharp
// Minimal APIs
app.MapGet("/api/products", async (int page, AppDbContext db) =>
{
return await db.Products
.Skip((page - 1) * 10)
.Take(10)
.ToListAsync();
})
.CacheOutput(b => b.VaryByQuery("page"));
// MVC Controllers
[HttpGet]
[OutputCache(Duration = 60, VaryByQueryKeys = new[] { "page", "category" })]
public async Task<IActionResult> GetProducts(
[FromQuery] int page = 1,
[FromQuery] string category = null)
{
var query = _db.Products.AsQueryable();
if (!string.IsNullOrEmpty(category))
query = query.Where(p => p.Category == category);
return Ok(await query.Skip((page - 1) * 10).Take(10).ToListAsync());
}
// Minimal APIs
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput(b => b.VaryByHeader("Accept-Language"));
// MVC Controllers
[HttpGet]
[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public async Task<IActionResult> GetProducts()
{
return Ok(await _db.Products.ToListAsync());
}
// Minimal APIs
app.MapGet("/api/products/{category}", async (string category, AppDbContext db) =>
{
return await db.Products.Where(p => p.Category == category).ToListAsync();
})
.CacheOutput(b => b.VaryByRouteValue("category"));
// MVC Controllers
[HttpGet("{category}")]
[OutputCache(Duration = 60, VaryByRouteParameters = new[] { "category" })]
public async Task<IActionResult> GetProductsByCategory(string category)
{
return Ok(await _db.Products.Where(p => p.Category == category).ToListAsync());
}
// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.Expire(TimeSpan.FromMinutes(1)));
// MVC Controllers
[HttpGet]
[OutputCache(Duration = 60)] // 60 seconds
public IActionResult GetProducts() => Ok(_service.GetProducts());
// Hoặc với sliding expiration (thêm mới)
[HttpGet]
[OutputCache(Duration = 300, SlidingExpiration = true)]
public IActionResult GetProducts() => Ok(_service.GetProducts());
// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.NoCache());
// MVC Controllers
[HttpGet]
[OutputCache(NoStore = true)]
public IActionResult GetProducts() => Ok(_service.GetProducts());
// Minimal APIs - Cache với tags
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput(b => b.Tag("products"));
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
return await db.Products.FindAsync(id);
})
.CacheOutput(b => b.Tag($"product-{id}"));
// Invalidate cache khi update
app.MapPut("/api/products/{id}", async (int id, Product product, AppDbContext db, IOutputCacheStore cache) =>
{
var existing = await db.Products.FindAsync(id);
if (existing == null) return Results.NotFound();
existing.Name = product.Name;
existing.Price = product.Price;
await db.SaveChangesAsync();
// Evict cache
await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
return Results.NoContent();
});
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _db;
private readonly IOutputCacheStore _cache;
public ProductsController(AppDbContext db, IOutputCacheStore cache)
{
_db = db;
_cache = cache;
}
[HttpGet]
[OutputCache(Duration = 300, Tags = new[] { "products" })]
public async Task<IActionResult> GetProducts()
{
return Ok(await _db.Products.ToListAsync());
}
[HttpGet("{id}")]
[OutputCache(Duration = 300, Tags = new[] { "product-{id}" })]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _db.Products.FindAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
var existing = await _db.Products.FindAsync(id);
if (existing == null) return NotFound();
existing.Name = product.Name;
existing.Price = product.Price;
await _db.SaveChangesAsync();
// Invalidate cache by tag
await _cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
await _cache.EvictByTagAsync("products", CancellationToken.None);
return NoContent();
}
}
---
## Response Caching (Client-side)
### Configuration
```csharp
// Add response caching middleware
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
// Minimal APIs
app.MapGet("/api/products", () => GetProducts());
// MVC Controllers
app.MapControllers();
app.MapGet("/api/products", () =>
{
var response = Results.Ok(GetProducts());
return response;
})
.AddEndpointFilter(async (context, next) =>
{
var response = await next(context);
context.HttpContext.Response.Headers["Cache-Control"] =
"public, max-age=60"; // Cache 60 seconds
return response;
});
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[ResponseCache(Duration = 60, VaryByHeader = "Accept-Language")]
public IActionResult GetProducts()
{
return Ok(_service.GetProducts());
}
}
// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.Expire(TimeSpan.FromMinutes(5)));
app.MapPost("/api/products", (Product p) => CreateProduct(p)); // Don't cache
// MVC Controllers
[HttpGet]
[OutputCache(Duration = 300)] // Cache read-heavy endpoints
public IActionResult GetProducts() => Ok(_service.GetProducts());
[HttpPost] // Don't cache write endpoints
public IActionResult CreateProduct(Product p) => Ok(_service.Create(p));
// Minimal APIs
app.MapGet("/api/products/{id}", (int id) => GetProduct(id))
.CacheOutput(b => b.Tag($"product-{id}"));
// Invalidate when updated
await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
// MVC Controllers
[HttpGet("{id}")]
[OutputCache(Tags = new[] { "product-{id}" })]
public IActionResult GetProduct(int id) => Ok(_service.GetProduct(id));
// Minimal APIs
app.MapGet("/api/products", (string? category, int page) => GetProducts(category, page))
.CacheOutput(b => b.VaryByQuery("category", "page"));
// MVC Controllers
[HttpGet]
[OutputCache(VaryByQueryKeys = new[] { "category", "page" })]
public IActionResult GetProducts(
[FromQuery] string category,
[FromQuery] int page) => Ok(_service.GetProducts(category, page));