Skip to content

Reading and writing cached data

Read cached responses with GetValue, invalidate with SetValue and RemoveValue, and apply read-modify-write updates with TransformValue.

4 min read

A generated cache exposes three public methods and one protected one. All four take the request, not the key — the cache calls your TransformKey for you.

MemberSignatureVisibility
GetValueValueTask<TResponse> GetValue(TRequest request, CancellationToken cancellationToken = default)public
SetValuevoid SetValue(TRequest request, TResponse value)public
RemoveValuevoid RemoveValue(TRequest request)public
TransformValueValueTask<TResponse> TransformValue(TRequest request, Func<TResponse, CancellationToken, ValueTask<TResponse>> transformer, CancellationToken token = default)protected

Reading

var response = await cache.GetValue(request, token);

If a value is cached it is returned immediately. Otherwise the cache creates a temporary DI scope, resolves the handler in it, executes it, stores the response, and disposes the scope.

Writing

cache.SetValue(request, response);

SetValue stores a response without executing the handler. It works on a key that has never been requested, which makes it useful for priming a cache from data you already have.

Invalidating

cache.RemoveValue(request);

RemoveValue discards the stored response so that the next GetValue re-executes the handler.

Read-modify-write with TransformValue

TransformValue reads the current value (waiting on an in-flight handler execution if there is one), passes it to your transformer, and stores the result. It is protected, so a cache author wraps it in a purpose-built public method:

GetUserCache.cs
[CacheFor<GetUser>]
public sealed partial class GetUserCache
{
	protected override string TransformKey(GetUser.Query request) =>
		$"GetUser({request.UserId})";

	public ValueTask<GetUser.Response> Rename(
		GetUser.Query request,
		string newName,
		CancellationToken token = default
	) =>
		TransformValue(
			request,
			(response, _) => ValueTask.FromResult(response with { Name = newName }),
			token
		);
}

If nothing is cached when TransformValue is called, it first triggers a normal handler execution to produce a value, then transforms it.

Concurrency and cancellation semantics

These rules follow from the shared, per-key execution slot that backs every cache entry.

One execution per key

Concurrent GetValue calls for the same key coalesce onto a single handler execution. Every caller receives the identical response instance.

Your token cancels only you

The CancellationToken you pass to GetValue is applied to your await, not to the shared handler execution. Cancelling it makes your GetValue throw, while the handler keeps running for the remaining callers and still populates the cache — so a subsequent GetValue finds the value already there.

This also means a handler with no callers left continues to completion rather than being torn down. Only SetValue and RemoveValue cancel the shared execution.

Exceptions reach every waiter

If the handler throws, the exception is propagated to every caller awaiting that key. The failure is not cached: the next GetValue starts a fresh execution.

Everything is per-process

The backing store is IMemoryCache. Coalescing, invalidation and transforms all apply to a single process only; nothing is broadcast to other instances.

Next: configuring cache entries.