A production-ready ASP.NET Core Web API demonstrating Output Caching with named policies, tag-based eviction, vary-by-route, and vary-by-query strategies — all with a clean Product Catalog domain.
If this sample saved you time, consider joining our Patreon community. You'll get exclusive .NET tutorials, premium code samples, and early access to new content — all for the price of a coffee.
👉 Join CodingDroplets on Patreon
Prefer a one-time tip? Buy us a coffee ☕
- The difference between Output Caching and Response Caching — and why Output Caching is superior for modern APIs
- How to register named Output Cache policies with custom TTLs
- How to vary cache entries by route value (
{id}) and by query string (?name=Electronics) - How to use cache tags (
products,category) for atomic bulk eviction - How to evict cache entries from inside a controller using
IOutputCacheStore - How to write unit tests and integration tests that cover the full HTTP pipeline
- Enterprise coding patterns: interface-based services, dependency injection, XML doc comments
HTTP Client
│
▼
┌─────────────────────────────────────────────────┐
│ ASP.NET Core Middleware Pipeline │
│ │
│ UseOutputCache() ◄── CACHE HIT? │
│ │ │ │
│ │ Cache MISS │ Cache HIT │
│ ▼ │ │
│ UseRouting() │ │
│ │ │ │
│ ▼ │ │
│ ProductsController │ │
│ │ │ │
│ ▼ │ │
│ IProductService │ ◄── Return cached │
│ (in-memory store) │ response │
│ │ │ directly │
│ ▼ │ │
│ Response generated ───────┘ │
│ + stored in cache │
└─────────────────────────────────────────────────┘
│
▼
HTTP Response
Cache eviction flow (POST / DELETE):
Client: POST /api/products
│
▼
ProductsController.Create()
│
├── Saves product to IProductService
│
└── IOutputCacheStore.EvictByTagAsync("products")
│
└── All cache entries tagged "products" are invalidated
Next GET requests hit the controller and repopulate the cache
| Policy Name | TTL | Vary By | Tags Applied | Used On |
|---|---|---|---|---|
Short |
30 sec | — | — | (available for extension) |
Long |
5 min | — | products |
GET /api/products |
VaryByCategory |
30 sec | name query param |
products, category |
GET /api/products/category |
VaryById |
5 min | id route value |
products |
GET /api/products/{id} |
dotnet-output-caching-api/
├── dotnet-output-caching-api.sln
│
├── OutputCaching.Api/ # Main Web API project
│ ├── Controllers/
│ │ └── ProductsController.cs # CRUD endpoints with [OutputCache] attributes
│ ├── Models/
│ │ └── Product.cs # Product entity
│ ├── Policies/
│ │ └── CacheConstants.cs # Policy names + tag constants (no magic strings)
│ ├── Services/
│ │ ├── IProductService.cs # Service contract
│ │ └── ProductService.cs # In-memory implementation
│ ├── Properties/
│ │ └── launchSettings.json # Visual Studio launch config (Swagger UI)
│ └── Program.cs # DI, policy registration, middleware pipeline
│
└── OutputCaching.Tests/ # xUnit test project
├── ProductServiceTests.cs # Unit tests for ProductService
└── ProductsControllerIntegrationTests.cs # Integration tests via WebApplicationFactory
| Requirement | Version |
|---|---|
| .NET SDK | 10.0+ |
| IDE | Visual Studio 2022 / JetBrains Rider / VS Code |
Check your SDK: dotnet --version
# 1. Clone the repository
git clone https://github.com/codingdroplets/dotnet-output-caching-api.git
cd dotnet-output-caching-api
# 2. Build the solution
dotnet build -c Release
# 3. Run the API
cd OutputCaching.Api
dotnet run
# 4. Open Swagger UI
# http://localhost:5289/swaggerThe browser opens automatically to the Swagger UI when launched from Visual Studio.
// Program.cs
builder.Services.AddOutputCache(options =>
{
// Short-lived: 30-second TTL
options.AddPolicy("Short", policy =>
policy.Expire(TimeSpan.FromSeconds(30)));
// Long-lived: 5-minute TTL
options.AddPolicy("Long", policy =>
policy.Expire(TimeSpan.FromMinutes(5)));
// Vary-by-query: one cache entry per unique "name" query value
options.AddPolicy("VaryByCategory", policy =>
policy
.Expire(TimeSpan.FromSeconds(30))
.SetVaryByQuery("name"));
// Vary-by-route: one cache entry per unique "id" route segment
options.AddPolicy("VaryById", policy =>
policy
.Expire(TimeSpan.FromMinutes(5))
.SetVaryByRouteValue("id"));
});// MUST come before app.MapControllers()
app.UseOutputCache();[HttpGet]
[OutputCache(PolicyName = "Long", Tags = ["products"])]
public IActionResult GetAll()
{
return Ok(_productService.GetAll());
}
[HttpGet("{id:int}")]
[OutputCache(PolicyName = "VaryById", Tags = ["products"])]
public IActionResult GetById(int id) { ... }[HttpPost]
public async Task<IActionResult> Create([FromBody] Product product, CancellationToken ct)
{
var created = _productService.Add(product);
// Immediately invalidate all cache entries tagged "products"
await _cacheStore.EvictByTagAsync("products", ct);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}| Method | Endpoint | Description | Status Codes |
|---|---|---|---|
GET |
/api/products |
List all products (cached 5 min) | 200 |
GET |
/api/products/{id} |
Get product by ID (cached 5 min per ID) | 200, 404 |
GET |
/api/products/category?name={cat} |
Filter by category (cached 30 sec/cat) | 200, 400 |
POST |
/api/products |
Add product + evict "products" cache | 201, 400 |
DELETE |
/api/products/{id} |
Delete product + evict "products" cache | 204, 404 |
dotnet test -c Release| Test Class | Type | Count |
|---|---|---|
ProductServiceTests |
Unit | 9 |
ProductsControllerIntegrationTests |
Integration | 9 |
| Total | 18 |
All 18 tests cover: CRUD operations, edge cases (missing IDs, empty names), 400/404 responses, and full HTTP pipeline behavior via WebApplicationFactory<Program>.
| Feature | Output Caching | Response Caching |
|---|---|---|
| Storage location | Server memory (in-process) | HTTP cache (proxy/browser) |
| Control | Full server-side control | Relies on HTTP headers (Cache-Control) |
| Tag-based eviction | ✅ Yes (EvictByTagAsync) |
❌ No |
| Vary by route/query | ✅ Built-in (SetVaryByRouteValue) |
Vary header only |
| Works with POST/DELETE | ✅ Can evict on mutation | ❌ Only caches GET/HEAD |
| Introduced | .NET 7 | .NET Core 1.0 |
Tags let you group related cache entries and invalidate them all atomically:
GET /api/products → tagged "products"
GET /api/products/1 → tagged "products"
GET /api/products/2 → tagged "products"
GET /api/products/category?name=Electronics → tagged "products", "category"
POST /api/products
└── EvictByTagAsync("products")
→ ALL four entries above are invalidated in one call
Without tags, you'd need to track and evict every cache key individually.
- ASP.NET Core 10 — Web API framework
- Output Caching (
Microsoft.AspNetCore.OutputCaching) — Server-side response cache - Swashbuckle / Swagger UI — API documentation and testing
- xUnit — Unit and integration testing framework
- WebApplicationFactory — In-process integration test host
- Output Caching Middleware in ASP.NET Core — Microsoft Docs
- IOutputCacheStore — evicting by tag — Microsoft Docs
- Caching in .NET overview — Microsoft Docs
This project is licensed under the MIT License — see LICENSE for details.
| Platform | Link |
|---|---|
| 🌐 Website | https://codingdroplets.com/ |
| 📺 YouTube | https://www.youtube.com/@CodingDroplets |
| 🎁 Patreon | https://www.patreon.com/CodingDroplets |
| ☕ Buy Me a Coffee | https://buymeacoffee.com/codingdroplets |
| 💻 GitHub | http://github.com/codingdroplets/ |
Want more samples like this? Support us on Patreon or buy us a coffee ☕ — every bit helps keep the content coming!