ASP.NET Core 10 APIs · Lesson 12

Output caching and private-response boundaries

Choose a public variation key without caching personalized work.

External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

Choose the representation that may be reused

Output caching reuses an HTTP response under a server-side policy. A domain dictionary, an HTTP output cache and a browser cache are different owners with different eligibility rules. The portable companion memoizes public topic values and bypasses reuse for private observations, producing deterministic generations. The external project applies real ASP.NET output caching only to a public catalog endpoint and varies it by the topic query value. A separate private endpoint is outside that policy and returns a no-store header. The example demonstrates explicit reuse boundaries without pretending to authorize users or persist a distributed cache.

Compare cache hits, variation and uncached work

The console prints one for the first public topic, one again for the repeated key, then two and three for direct private computations. On a fresh external server, the first csharp catalog request creates generation one. Repeating that topic reuses generation one. A dotnet topic produces generation two because the variation key differs. Two private calls produce generations three and four and the first carries the explicit no-store response header. These observations compare values, not request duration; a fast response alone is not reliable evidence that the output cache supplied it.

Every reusable input dimension belongs in the contract

The catalog representation depends only on topic and a generated observation, so the public policy deliberately varies by topic. A real representation may also depend on locale, tenant, permissions or other inputs. Omitting a relevant dimension can return one caller's content to another even when the cache works exactly as configured. The companion project's tenant-and-topic keys explore this ownership issue independently of HTTP. Do not add a raw user identifier to a public teaching policy as though that alone establishes a safe personalized cache. First decide whether the representation should be shared at all and who is permitted to receive it.

Keep framework eligibility rules and private work explicit

The policy uses the framework's baseline rules instead of deliberately overriding authentication or cookie safeguards. This application does not configure authentication, and its public tests are anonymous. The private route has no CacheOutput metadata, so it is not included in the named policy; its no-store header communicates another cache boundary rather than substituting for that server-side choice. A header alone is not an authorization check or a universal guarantee about every intermediary. Likewise, a cached response should not hide a state mutation, payment or per-request secret behind an apparently harmless GET endpoint.

Practice: challenge the public variation dimension

Request csharp twice and dotnet once on a fresh server, checking the independently expected generations. Then add a new representation dimension such as a fixed supported locale. First specify its expected outputs and whether it belongs in the cache key; implement the variation only after that decision. Use distinct locale values while keeping topic constant so the test can reveal an omitted dimension. The hint is to compare representation contents and generation behavior, not just whether every request returns 200. A successful status cannot identify a cache-key collision.

Practice: keep private observations out of shared reuse

Add a private response field derived from a deliberately bounded local test context and verify that repeated private requests still execute independently. Keep that endpoint outside the public policy. In the portable tenant-variation project, compare alpha and beta for the same topic, clear the owned cache and verify a new public generation. These exercises test different owners: one is real HTTP output eligibility, the other is a domain key and lifetime. Do not call the dictionary project an ASP.NET cache test, because it has neither middleware nor response metadata.

Review eligibility, variation and lifetime together

A cache review should identify what can be reused, which callers may share it, every input affecting the representation and when reuse ends. The sixty-second expiration belongs to the external output policy; the simple console dictionary intentionally has no TTL. Preserve real endpoint checks when changing middleware or metadata, and keep domain key tests independent when business dimensions grow. The original generations provide useful evidence for one anonymous public policy and one explicitly uncached private route. They do not prove distributed invalidation, authentication integration or the behavior of a browser and proxy chain that the project never configures.

  • Vary reusable public output by every relevant representation dimension.
  • Server output policy, client cache headers and authorization have separate responsibilities.
External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Output caching and private-response boundaries — real ASP.NET Core 10 project

Set up the SDK project

  1. Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
  2. From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
  3. Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
  4. Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
  5. Call /catalog?topic=csharp twice, then /catalog?topic=dotnet, then /private twice. Inspect generations and Cache-Control.

Download complete SDK project ↗

Project files

WebLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

  </PropertyGroup>
</Project>
global.jsonDownload file ↗
{
  "sdk": {
    "version": "10.0.403",
    "rollForward": "latestPatch"
  }
}
Program.csDownload file ↗
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options => options.AddPolicy("public-topic", policy => policy.Expire(TimeSpan.FromSeconds(60)).SetVaryByQuery("topic")));
var app = builder.Build(); app.UseOutputCache(); int generation = 0;
app.MapGet("/catalog", (string? topic) => new { topic = topic ?? "all", generation = Interlocked.Increment(ref generation) }).CacheOutput("public-topic");
app.MapGet("/private", (HttpContext context) => { context.Response.Headers.CacheControl = "no-store"; return new { generation = Interlocked.Increment(ref generation) }; }); app.Run();

Expected result

Repeated anonymous GET /catalog?topic=csharp reuses generation 1; topic=dotnet produces generation 2. Two /private calls are uncached generations 3 and 4 and carry Cache-Control: no-store.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var cache = new Dictionary<string, int>(StringComparer.Ordinal); int generation = 0;
int Public(string topic) { if (!cache.TryGetValue(topic, out int value)) cache[topic] = value = ++generation; return value; }
Console.WriteLine(Public("csharp")); Console.WriteLine(Public("csharp")); Console.WriteLine(++generation); Console.WriteLine(++generation);
Open C# practice

C# practice output

1
1
2
3
Try this
Public output variation and private computation have different reuse policies; cache keys are part of the contract.

Further reading