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.
When to Use
Section titled “When to Use”- 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)
Installation
Section titled “Installation”dotnet add package Shiny.DocumentDb.Redis-
Direct instantiation
using Shiny.DocumentDb.Redis;var store = new RedisDocumentStore(new RedisDocumentStoreOptions{ConnectionString = "localhost:6379"}); -
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.SEARCHo.MapVersionProperty<Order>(x => x.Version); // opt-in optimistic concurrency});AddRedisDocumentStoreregistersIDocumentStoreandIDocumentMaintenanceas singletons. You can also pass a pre-builtIConnectionMultiplexerviao.Multiplexer.
Options Reference
Section titled “Options Reference”| Property | Type | Default | Description |
|---|---|---|---|
ConnectionString | string? | null | StackExchange.Redis connection string |
Multiplexer | IConnectionMultiplexer? | null | Pre-built multiplexer (wins over the connection string) |
Database | int | -1 | Redis logical database index |
KeyNamespace | string | "" | Prefix applied to all keys/indexes (multi-tenant / test isolation) |
TypeNameResolution | TypeNameResolution | ShortName | How type names are derived |
JsonSerializerOptions | JsonSerializerOptions? | null | JSON serialization settings |
UseReflectionFallback | bool | true | Set 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.
Storage & the search index
Section titled “Storage & the search index”| Concept | Redis |
|---|---|
| Document | RedisJSON key doc:{typeName}:{id} (JSON.SET/JSON.GET/JSON.MERGE) |
| Index | one FT.CREATE … ON JSON PREFIX doc:{typeName}: per type, built lazily |
| Queryable fields | only fields declared via MapIndexedProperty/MapFullTextProperty/MapVectorProperty/MapSpatialProperty |
| Concurrency (CAS) | mapped version field guarded by a Lua compare-and-set |
| Int/Long Id counter | INCR seq:{typeName} |
Querying
Section titled “Querying”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 / BM25await store.NearestVectors<Doc>(queryVector, k: 10); // VECTOR HNSW KNNawait store.WithinRadius<Place>(centre, radiusMeters); // GEOServer-side GROUPBY + COUNT/SUM runs through FT.AGGREGATE. The string Query/QueryStream/Count(whereClause) (SQL WHERE) overloads throw — use the LINQ surface.
Ids & concurrency
Section titled “Ids & concurrency”- 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 atomic —
JSON.SET … NXmeans concurrent inserts of the same id race safely (one wins, the rest throwInvalidOperationException). - Optimistic concurrency:
MapVersionProperty<T>seeds the version to 1 on insert and guards updates with a Lua compare-and-set; a stale write throwsConcurrencyException.
Change observation
Section titled “Change observation”// In-process — this store instance's own writesawait 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.
Limitations
Section titled “Limitations”- Redis Stack required — RedisJSON + RediSearch modules must be loaded.
- Push-down needs declared fields — a
Whereover a property without aMap*Propertymapping 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.