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!