Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Redis

The Shiny.DocumentDb.Redis package provides a document store over Redis Stack using NRedisStack. Each document is stored as a RedisJSON key (doc:{typeName}:{id}) and each type gets a RediSearch index, so filtering, full-text, vector (KNN), and geo queries run server-side.

NuGet package Shiny.DocumentDb.Redis
  • A hot/caching tier that also needs real document queries, full-text, or vector search
  • Low-latency lookups with server-side GROUPBY/aggregation (FT.AGGREGATE)
  • You want atomic, monotonic Int/Long Id auto-generation (unique to this provider among the NoSQL backends)
Terminal window
dotnet add package Shiny.DocumentDb.Redis
  1. Direct instantiation

    using Shiny.DocumentDb.Redis;
    var store = new RedisDocumentStore(new RedisDocumentStoreOptions
    {
    ConnectionString = "localhost:6379"
    });
  2. Dependency injection

    using Shiny.DocumentDb;
    builder.Services.AddRedisDocumentStore(o =>
    {
    o.ConnectionString = "localhost:6379";
    o.MapIndexedProperty<Order>(x => x.Status); // push Where(x => x.Status == …) into FT.SEARCH
    o.MapVersionProperty<Order>(x => x.Version); // opt-in optimistic concurrency
    });

    AddRedisDocumentStore registers IDocumentStore and IDocumentMaintenance as singletons. You can also pass a pre-built IConnectionMultiplexer via o.Multiplexer.

PropertyTypeDefaultDescription
ConnectionStringstring?nullStackExchange.Redis connection string
MultiplexerIConnectionMultiplexer?nullPre-built multiplexer (wins over the connection string)
Databaseint-1Redis logical database index
KeyNamespacestring""Prefix applied to all keys/indexes (multi-tenant / test isolation)
TypeNameResolutionTypeNameResolutionShortNameHow type names are derived
JsonSerializerOptionsJsonSerializerOptions?nullJSON serialization settings
UseReflectionFallbackbooltrueSet false for AOT safety

Mapping methods: MapIdProperty/MapIdType, MapVersionProperty, MapIndexedProperty (TAG/NUMERIC schema fields), MapFullTextProperty (TEXT), MapVectorProperty (VECTOR/KNN), MapSpatialProperty (GEO), MapComputedProperty, AddQueryFilter, and the interceptor hooks.

ConceptRedis
DocumentRedisJSON key doc:{typeName}:{id} (JSON.SET/JSON.GET/JSON.MERGE)
Indexone FT.CREATE … ON JSON PREFIX doc:{typeName}: per type, built lazily
Queryable fieldsonly fields declared via MapIndexedProperty/MapFullTextProperty/MapVectorProperty/MapSpatialProperty
Concurrency (CAS)mapped version field guarded by a Lua compare-and-set
Int/Long Id counterINCR seq:{typeName}

LINQ predicates over declared index fields push down to FT.SEARCH (ToQueryString() returns the emitted RediSearch query); the full predicate is always re-applied client-side, so results are exact even for fields you didn’t index.

var open = await store.Query<Order>()
.Where(o => o.Status == "Open" && o.Total > 100) // @status + @total → FT.SEARCH
.OrderBy(o => o.Total)
.Paginate(0, 25)
.ToList();

Native rich-search terminators (require the corresponding Map*Property):

await store.FullTextSearch<Article>("distributed systems"); // RediSearch TEXT / BM25
await store.NearestVectors<Doc>(queryVector, k: 10); // VECTOR HNSW KNN
await store.WithinRadius<Place>(centre, radiusMeters); // GEO

Server-side GROUPBY + COUNT/SUM runs through FT.AGGREGATE. The string Query/QueryStream/Count(whereClause) (SQL WHERE) overloads throw — use the LINQ surface.

  • Guid / string Ids auto-generate on Insert when default.
  • Int/Long Id auto-generation works — a per-type INCR seq:{typeName} counter assigns the next value. This is a differentiator: every other NoSQL provider throws for default int/long Ids. (Ids are monotonic-per-store, not globally meaningful.)
  • Insert is atomicJSON.SET … NX means concurrent inserts of the same id race safely (one wins, the rest throw InvalidOperationException).
  • Optimistic concurrency: MapVersionProperty<T> seeds the version to 1 on insert and guards updates with a Lua compare-and-set; a stale write throws ConcurrencyException.
// In-process — this store instance's own writes
await foreach (var c in ((IObservableDocumentStore)store).NotifyOnChange<Order>(ct)) …
// Cross-process — keyspace notifications (requires notify-keyspace-events on the server)
await using var sub = await ((IChangeFeedDocumentStore)store).SubscribeChanges<Order>(async (c, ct) => …);

The keyspace-notification feed is best-effort (not durable) and surfaces writes as Updated.

  • Redis Stack required — RedisJSON + RediSearch modules must be loaded.
  • Push-down needs declared fields — a Where over a property without a Map*Property mapping falls back to a client-side filter.
  • String Query/Count(whereClause) (SQL WHERE) throw — use LINQ.
  • Compensating unit of work — buffered writes apply atomically via MULTI/EXEC; there is no read-your-writes transaction.