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.

Get started dotnet add package Prague
Prague logo: a line drawing of Franz Kafka beside the word Prague and a database icon

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.

How Prague differs from a remote cache and from MemoryCache
AspectPragueRedisMemoryCache
Where data livesIn your processSeparate server over the networkIn your process
Query modelIndexed fluent queries, joins, OR, sorted pagingKey lookups, Lua scriptsKey lookups
Type safetyGenerated at compile timeRuntime stringsRuntime keys and casts
Allocations per read0 B, pooled resultsSerialization + network buffersPer object
Source of truthCompacted Kafka topicItselfItself
EvictionNone, topic compaction is the retentionTTL, LRUTTL, 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.Api

Every 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