Skip to content

codingdroplets/dotnet-output-caching-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

dotnet-output-caching-api

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.

Visit CodingDroplets YouTube Patreon Buy Me a Coffee GitHub


πŸš€ Support the Channel β€” Join on Patreon

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 β˜•


🎯 What You'll Learn

  • 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

πŸ—ΊοΈ Architecture Overview

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

πŸ“‹ Cache Policy Summary

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}

πŸ“ Project Structure

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

πŸ› οΈ Prerequisites

Requirement Version
.NET SDK 10.0+
IDE Visual Studio 2022 / JetBrains Rider / VS Code

Check your SDK: dotnet --version


⚑ Quick Start

# 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/swagger

The browser opens automatically to the Swagger UI when launched from Visual Studio.


πŸ”§ How It Works

Step 1 β€” Register Output Cache services

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

Step 2 β€” Add middleware (before routing)

// MUST come before app.MapControllers()
app.UseOutputCache();

Step 3 β€” Apply policy on endpoints with [OutputCache]

[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) { ... }

Step 4 β€” Evict by tag after writes

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

πŸ“‘ API Endpoints

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

πŸ§ͺ Running Tests

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>.


πŸ€” Key Concepts

Output Caching vs Response Caching

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) ⚠️ Via Vary header only
Works with POST/DELETE βœ… Can evict on mutation ❌ Only caches GET/HEAD
Introduced .NET 7 .NET Core 1.0

Why Tag-Based Eviction?

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.


🏷️ Technologies Used

  • 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

πŸ“š References


πŸ“„ License

This project is licensed under the MIT License β€” see LICENSE for details.


πŸ”— Connect with CodingDroplets

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!

About

ASP.NET Core Web API demonstrating Output Caching with named policies, tag-based eviction, vary-by-route, and vary-by-query cache strategies. Includes 18 tests. Built with .NET 10.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages