Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Output Caching

Overview Questions

  • 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ì?

Output Caching vs Response Caching

Comparison

┌─────────────────────────────────────────────────────────────────┐
│                    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                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Output Caching Configuration

Basic Setup

// 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();

MVC Controllers Setup

// 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();

Cache Policies

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"));
});

Endpoint Caching

Basic Caching (Minimal APIs)

// Cache với default policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput();

Basic Caching (MVC Controllers)

[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());
    }
}

Cache với custom policy (Controllers)

[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());
}

Vary By Header

// 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());
}

Vary By Route

// 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());
}

Cache Expiration

Time-based Expiration

// 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());

No Caching

// Minimal APIs
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.NoCache());

// MVC Controllers
[HttpGet]
[OutputCache(NoStore = true)]
public IActionResult GetProducts() => Ok(_service.GetProducts());

Cache Tags

Tag-based Invalidation

// 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();
});

Tag-based Invalidation (MVC Controllers)

[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();

Cache Headers (Minimal APIs)

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;
});

Cache Headers (MVC Controllers)

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [ResponseCache(Duration = 60, VaryByHeader = "Accept-Language")]
    public IActionResult GetProducts()
    {
        return Ok(_service.GetProducts());
    }
}

Best Practices

1. Cache Appropriately

// 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));

2. Use Tags for Invalidation

// 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));

3. Vary by Appropriate Keys

// 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));