In-memory event cache for .NET
Prague
Caching
Prague turns event-sourced Kafka topics into typed, indexed, queryable views inside your process. A Roslyn source generator does the wiring, so queries are checked at compile time and results are pooled with zero allocations.
dotnet add package Prague
Concurrent read throughput11M+reads per second
Attributes in, queries out
Every [DataCache] type is partial. The generator emits the cache class, the index storage and a fluent builder whose methods are named after your indices. Joins come from foreign-key attributes, so the join surface is also generated.
[DataCache]
public partial class Product {
[DataCacheKey] public int Id { get; set; }
[DataCacheIndex(DataCacheIndexType.Range)]
public long ReleaseDate { get; set; }
public bool IsPublished { get; set; }
}
[DataCache]
public partial class Offer {
[DataCacheKey] public int Id { get; set; }
[DataCacheForeignKey<Product>(DataCacheJoinType.OneToMany)]
public int ProductId { get; set; }
public decimal Price { get; set; }
}
// Generated builder: compile-time checked, pooled, 0 B per execution.
using var rows = productCache.Query()
.WithReleaseDate(q => q.Gte(now)) // range index
.Where(p => p.IsPublished) // residual filter on survivors
.JoinWithOffer() // 1:N join generated from the FK
.ExecutePooled(); // rents from ArrayPool; Dispose() returns it
foreach (var row in rows) {
var product = row.Left; // Product
var offers = row.Right; // QueryResults<Offer>
}Core architecture
Extreme performance
Source generators do the wiring, so nothing is reflected over on the read path. Spans, stackalloc bitmaps, ArrayPool buffers and lock-free readers keep the hot path allocation-free.
Compile-time safety
IntelliSense lists exactly the indices you declared. Which verbs are reachable where is enforced by the type system, not by a runtime check.
Built for event sourcing
Kafka consumer and producer on a raw, span-based zero-copy path. Header, key and value filters, tombstones, and conditional dispatch that skips unchanged values.
Automatic query planning
Unique, Many and Range indices plus symmetric and key-set variants. Every WithXxx narrows candidates; intersection short-circuits on the first empty lane.
Global last-update index
Cross-cache change tracking for incremental sync: ask for everything that changed since a timestamp.
The numbers
- 11M+
- reads per second, pooled 3-level join, 10 reader threads + 2 writers
- < 100 ns
- indexed lookup
- 18 ns
- frozen unique-key point lookup
- 0 B
- per execution on pooled and frozen paths
All figures are from an Apple M4 Pro and vary by hardware. Reads per second is the sum of result rows returned divided by wall-clock seconds on a pooled three-level join with 10 reader threads and concurrent writers. Every measured table lives in RESULTS.MD.
| Aspect | Prague | Redis | MemoryCache |
|---|---|---|---|
| Where data lives | In your process | Separate server over the network | In your process |
| Query model | Indexed fluent queries, joins, OR, sorted paging | Key lookups, Lua scripts | Key lookups |
| Type safety | Generated at compile time | Runtime strings | Runtime keys and casts |
| Allocations per read | 0 B, pooled results | Serialization + network buffers | Per object |
| Source of truth | Compacted Kafka topic | Itself | Itself |
| Eviction | None, topic compaction is the retention | TTL, LRU | TTL, LRU |
What Prague is
- A library, not a server. Caches live in your process. There is no network hop.
- Generated, not reflected. Storage, indices, joins and builders come from attributes; the plan is fixed by your call shape at compile time.
- Pooled. Result sets rent from ArrayPool and return on Dispose.
What Prague is not
- Not a distributed cache. Each process owns its caches; replication is the upstream log's job.
- Not a relational database. Fluent expressions over typed indices, not SQL.
- Not an eviction cache. No TTL or LRU. The topic's compaction is the retention policy.
Packages
dotnet add package Prague # facade: Core + Attributes + Codegen
dotnet add package Prague.Kafka # consumer/producer, filters, health checks, OpenTelemetry
dotnet add package Prague.Api # optional: HTTP inspection endpoints
dotnet add package Prague.Api.UI # optional: Blazor UI over Prague.ApiEvery package targets net9.0 and net10.0. Published versions are on NuGet. MIT licensed.
Fast as thought.
Built on modern .NET for real-time systems that read far more than they write.
Read the documentation