ワンクリックで
koan-caching
Transparent L1/L2 caching for Entity<T>, [Cacheable] attribute, cross-node coherence, per-request opt-out
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Transparent L1/L2 caching for Entity<T>, [Cacheable] attribute, cross-node coherence, per-request opt-out
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Auto-registration via KoanAutoRegistrar, minimal Program.cs, "Reference = Intent" pattern
Chat endpoints, embeddings, RAG workflows, vector search
Aggregate boundaries, relationships, lifecycle hooks, value objects
Entity<T> patterns, GUID v7 auto-generation, static methods vs manual repositories
Run a mandatory pre-implementation exploration workflow before writing production code in Koan (.NET/C#). Use when a task requires code changes and Codex must first map concerns/layers, read relevant files and docs, check existing constants and types, identify the closest existing pattern, plan exact code placement, and confirm architectural guardrails.
EntityController<T>, custom routes, payload transformers, auth policies
| name | koan-caching |
| description | Transparent L1/L2 caching for Entity<T>, [Cacheable] attribute, cross-node coherence, per-request opt-out |
[Cacheable] or [CachePolicy] attributesEntityContext.NoCache() / RefreshCache() / WithCacheBehavior(...)CacheKey.For<T>(...), Cache.WithJson<T>(...), Cache.Evict<T,K>(...)Koan.Cache / Koan.Cache.Adapter.Redis / Koan.Cache.Adapter.Sqlite / Koan.Cache.Coherence.*Cache-Control headers or X-Koan-CacheReference = Intent. Add Koan.Cache to project references and [Cacheable] to an entity — that's the whole 90% case. The framework handles wiring, topology resolution, coherence, eviction, and instrumentation automatically.
[Cacheable(300)] // 5-minute TTL, L1=150s derived, Layered, GetOrSet
public sealed class Todo : Entity<Todo>
{
public string Title { get; set; } = "";
}
// Use normally — caching is transparent
var todo = await Todo.Get(id); // first hit DB, populates cache
var fast = await Todo.Get(id); // L1 hit, sub-ms
todo.Title = "updated";
await todo.Save(); // cache write-through + broadcasts EvictKey to peers
| Add this reference | Effect |
|---|---|
Koan.Cache | L1=Memory, single-node, no coherence |
+ Koan.Cache.Adapter.Sqlite | L1=SQLite (persistent across restart) |
+ Koan.Cache.Adapter.Redis | L2=Redis + coherence auto-activates |
+ Koan.Cache.Coherence.Messaging | Rides existing Koan.Messaging bus (preempts Redis pub/sub) |
+ Koan.Cache.Coherence.InMemory | In-process channel — tests / single-process verification |
| Pillar | Owns | Contract |
|---|---|---|
| Storage | K/V verbs | ICacheStore (Memory/SQLite/Redis) |
| Coherence | Cross-node invalidation | ICacheCoherenceChannel (Redis pub/sub, Koan.Messaging, in-memory) |
| Topology | L1/L2 wiring + write/evict orchestration | LayeredCache (internal) + CoherenceCoordinator (IHostedService) |
| Policy | Per-entity declarative intent | [Cacheable] / [CachePolicy] + CachePolicyMaterializer |
Critical invariant: ICacheStore has NO publish methods. Coherence is its own pillar with its own contract. LayeredCache.ApplyRemoteInvalidation touches L1 only — never L2 (shared, already evicted by writer), never republishes (would create feedback loops).
| If you see | Suggest |
|---|---|
services.AddMemoryCache() + manual cache wrapper | [Cacheable] on the entity instead. |
IMemoryCache injected into a service for entity caching | Same — entity caching is policy-driven, not DI-imperative. |
Manual redis.GetDatabase().StringGetAsync(...) | Cache.WithJson<T>(key).GetOrAdd(...) for arbitrary values; [Cacheable] for entities. |
| Custom cache-invalidation pub/sub | [Cacheable] writes broadcast automatically. For non-entity cases, use ICacheCoherenceChannel directly. |
| String concatenation for cache keys | CacheKey.For<TEntity>(id, partition) — canonical, partition-aware. |
Cache-bypass via if (skipCache) ... branches in business code | EntityContext.NoCache() / RefreshCache() scopes — declarative, doesn't pollute call sites. |
| L1 TTL > L2 TTL | Boot-time validator throws. The materializer enforces the invariant. |
Default reads are fresh-or-null — past AbsoluteTtl the cache returns null. To allow stale reads, callers must explicitly opt in via AllowStaleFor:
// Per-entity opt-in
[Cacheable(60, AllowStaleForSeconds: 10)]
public sealed class HotKey : Entity<HotKey> { }
// Per-call opt-in
var report = await Cache.WithJson<UsageReport>(key)
.WithAbsoluteTtl(TimeSpan.FromMinutes(5))
.AllowStaleFor(TimeSpan.FromMinutes(2)) // ← explicit; otherwise strict
.GetOrAdd(async ct => ..., ct);
There are no global SWR toggles in adapter options or app configuration. The per-call/per-policy opt-in is the only switch. The boot report flags policies that opt in: Policy:HotKey ... swr=10s [opt-in].
using (EntityContext.NoCache()) // CacheBehavior.Bypass
{
var fresh = await Todo.Get(id); // skip read, hit DB, no populate
}
using (EntityContext.RefreshCache()) // CacheBehavior.Refresh
{
var rebuilt = await Todo.Get(id); // skip read, hit DB, repopulate
}
using (EntityContext.WithCacheBehavior(CacheBehavior.ReadOnly))
{
var cached = await Todo.Get(id); // cache if present, no populate on miss
}
Writes always invalidate regardless of the scope — peer L1 entries are evicted via coherence even when bypass mode suppresses the local populate. Prevents silent multi-node desync.
app.UseKoanCacheControl() middleware (opt-in, in Koan.Web) maps standard HTTP cache headers onto EntityContext.CacheBehavior:
Cache-Control: no-cache → RefreshCache()Cache-Control: no-store → NoCache()X-Koan-Cache: refresh|bypass|readonly|default (wins over Cache-Control)Koan.Data.Direct and batch jobs bypass the cache decorator. Use the canonical evict surface:
await Cache.Evict<Todo, string>(id, ct); // single key + broadcast
await EntityCacheExtensions.Cache<Todo, string>().FlushAll(); // all entries tagged with "Todo"
await todo.Uncache(); // instance form
For computed/expensive values that aren't Entity<T>:
var report = await Cache.WithJson<UsageReport>($"report:{tenantId}")
.WithAbsoluteTtl(TimeSpan.FromHours(1))
.WithTags("reports", $"tenant:{tenantId}")
.GetOrAdd(async ct => await BuildReportAsync(tenantId, ct), ct);
await Cache.Tags($"tenant:{tenantId}").Flush(ct); // bulk invalidation
app.MapHealthChecks("/health") — picks up "koan-cache" automaticallyAddMeter("Koan.Cache") + AddSource("Koan.Cache")Koan:Cache:CoherenceMode = "Required" in production to fail fast if no coherence channel is registered with a Remote tier presentKOAN_CACHE_TRACE_KEY=Todo:_:abc-123 env var and restart — every touch on that key logs at Information