# Redis OM .NET: a document model and a query language on top of Redis Stack

> Redis OM .NET gives a .NET application a declarative object model over Redis, where a C# class with attributes becomes a stored document with a secondary index, and queries are written as LINQ expressions rather than raw commands. Its most instructive corner is the embedded document support, where an attribute takes a JSON path so you can index a field inside a nested object, and the documentation is honest about the limits of indexing arrays of objects.

**redis/redis-om-dotnet** — Object mapping, and more, for Redis and .NET

- Repository: https://github.com/redis/redis-om-dotnet
- Stars: 528 · Forks: 87
- Language: C#
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/redis-redis-om-dotnet

## What the library gives you

Redis OM .NET makes it easy to model Redis data in your .NET applications, and it is one of a family. The README puts it in a table with Redis OM Node.js, Redis OM Spring and Redis OM Python, so the same idea is available in several stacks, and the object mapping model is the common thread across them.

The feature list is four items. Declarative object mapping for Redis objects. Declarative secondary-index generation. Fluent APIs for querying Redis. And fluent APIs for performing Redis aggregations.

The middle two are the ones that matter. It is easy to serialise a class to JSON and put it in a hash, and it is easy to write a query by hand. What is not easy is making a query that is fast, and the answer to that is a secondary index maintained by Redis itself rather than a scan. Generating that index from the class definition means the query you write against the model is the query Redis can answer efficiently, without the application having to know which index backs it.

The primary language is C#, the licence is MIT, the default branch is main, and the recent releases are v1.3.0 from 2026-06-24, v1.2.0 from 2026-06-02 and v1.0.1 from 2025-06-27. The last push was on 2026-08-29 with the project not archived, and the repository publishes no GitHub releases. The version at 1.x with three releases in 2026 suggests the API is still settling, and the section on schema migration below is the clearest evidence of that.

The topics are dotnet, linq and redis, and the phrase and more in the repository description is doing some work, because the aggregations and the full text search are separate capabilities rather than extras.

## Installing it, and the Redis Stack requirement

Installation is a single command through the dotnet cli:

```text
dotnet add package Redis.OM
```

But the getting started section is more demanding than that, and the reason is stated before any code is written. Before writing any code you will need a Redis instance with the appropriate Redis modules, and the quickest way to get this is with Docker:

```sh
docker run -p 6379:6379 -p 8001:8001 redis/redis-stack
```

That launches redis-stack, an extension of Redis that adds modern data structures to Redis. And there is a second detail in the same paragraph that is easy to miss: if you open http://localhost:8001 you have access to the redis-insight GUI, a graphical tool for visualising and working with your data in Redis.

So the requirement is not just a Redis server, it is Redis Stack with the search and JSON modules loaded, because that is what indexes JSON documents and answers the queries the library generates. A plain Redis will start and then fail at the first query, and the failure will look like a missing index rather than a missing module.

The redis-insight port being mapped alongside the Redis port in the same command is a convenience, and it points at how the library is meant to be used. You are developing against a document model, so you want to look at what actually landed in the index, and a GUI for that is worth the extra port.

The repository carries a docker/ directory and a lowercase dockerfile, which is the deployment side of the same requirement, and a global.json for the .NET SDK version the solution expects.

## Modelling the domain, and creating the index

The model is a C# class with attributes, and the example is a customer.

```csharp
[Document(StorageType = StorageType.Json)]
public class Customer
{
   [Indexed] public string FirstName { get; set; }
   [Indexed] public string LastName { get; set; }
   public string Email { get; set; }
   [Indexed(Sortable = true)] public int Age { get; set; }
   [Indexed] public string[] NickNames {get; set;}
}
```

The Document attribute is applied to the class, and it carries a StorageType of StorageType.Json, so the object is stored as a JSON document rather than as a flat hash. Then individual properties carry the Indexed attribute, and one of them carries Indexed with Sortable set to true.

So the declaration says three things at once: how the object is stored, which fields are searchable, and which of them can be used for ordering. A field without the attribute, in this case Email, is stored but not indexed, which is the right default since indexing every field costs write throughput and memory.

Note that NickNames is a string array and is indexed, so the model supports indexing into collections as well as scalars.

Then you create the index, and it takes two lines:

```csharp
var provider = new RedisConnectionProvider("redis://localhost:6379");
provider.Connection.CreateIndex(typeof(Customer));
```

The provider is created with a Redis connection string, and CreateIndex is called with the type. There is no index definition to write, no JSON schema to hand over, and no call that says which fields to index, because all of that is derived from the attributes on the class.

That is the whole point of the declarative model, and it is why the secondary index generation is listed as a feature rather than a detail. The index in Redis and the attributes on your class are two views of one declaration, and the library is responsible for keeping them consistent.

The migration story is where the design meets reality, and the README is direct about the limits. Redis OM provides limited support for schema migration at this time. You can check if the index definition in Redis matches your current index definition using the IsIndexCurrent method on the RedisConnection, and then use that output to determine when to re-create your indexes when your types change:

```csharp
var provider = new RedisConnectionProvider("redis://localhost:6379");
var definition = provider.Connection.GetIndexInfo(typeof(Customer));

if (!provider.Connection.IsIndexCurrent(typeof(Customer)))
{
    provider.Connection.DropIndex(typeof(Customer));
    provider.Connection.CreateIndex(typeof(Customer));
}
```

So the pattern is a check, a conditional drop, and a create. GetIndexInfo gives you the definition currently in Redis, IsIndexCurrent compares it against the type you have now, and if they differ you drop and recreate.

That is not a migration system, it is a detection mechanism with a manual remedy, and the wording is careful not to claim otherwise. It is enough to keep a deployment from silently running against a stale index, which is the failure that matters, but you are expected to write the drop-and-create step yourself.

## Embedded documents, JSON paths and the array limitation

The embedded document support is the most interesting part of the library, and the README spends a lot of space on it because it is where the indexing model gets subtle.

There are two methods for indexing embedded documents, and an embedded document is a complex object, for example an Address property on the customer model:

```csharp
[Document(IndexName = "address-idx", StorageType = StorageType.Json)]
public partial class Address
{
    public string StreetName { get; set; }
    public string ZipCode { get; set; }
    [Indexed] public string City { get; set; }
    [Indexed] public string State { get; set; }
    [Indexed(CascadeDepth = 1)] public Address ForwardingAddress { get; set; }
    [Indexed] public GeoLoc Location { get; set; }
    [Indexed] public int HouseNumber { get; set; }
```

The address class carries its own Document attribute with an IndexName of address-idx, and the Indexed attributes on City, State, HouseNumber and Location, where Location is a GeoLoc. The ForwardingAddress property carries Indexed with CascadeDepth set to 1, which is the recursion control for an address inside an address.

The second method is to index by JSON path. You can index fields by JSON path in the top level model, decorating the Address property with Indexed and or Searchable and specifying the JSON path to the desired field:

```csharp
[Document(StorageType = StorageType.Json)]
public class Customer
{
   [Indexed] public string FirstName { get; set; }
   [Indexed] public string LastName { get; set; }
   public string Email { get; set; }
   [Indexed(Sortable = true)] public int Age { get; set; }
   [Indexed] public string[] NickNames {get; set;}
   [Indexed(JsonPath = "$.ZipCode")]
   [Searchable(JsonPath = "$.StreetAddress")]
   public Address Address {get; set;}
```

So a plain Indexed on the Address property would index the whole nested object, and Indexed with JsonPath of $.ZipCode indexes the zip code inside it, while Searchable with a JSON path marks a field for full text search. Having Indexed and Searchable as separate concepts on the same property is a meaningful distinction, since one is for equality and range filters and the other is for text analysis.

The same approach reaches into arrays and lists of objects:

```csharp
[Indexed(JsonPath = "$.City")]
public Address[] Addresses { get; set; }
```

And the cities can then be queried with an Any predicate within the main Where clause:

```csharp
collection.Where(c=>c.Addresses.Any(a=>a.City == "Satellite Beach"))
```

Then the limitations section, which is the part worth reading twice. The way Redis indexes fields within a collection of embedded objects does not allow multiple predicates to be specified to a given document.

```csharp
collection.Where(c=>c.Addresses.Any(a=>a.City == "Satellite Beach" && a.ZipCode == "32937))
```

And the explanation of what that means in practice. The query can only check if the Addresses collection contains an entry that is Satellite Beach, and contains an entry that has a zip code of 32937, rather than an entry that has both the city of Satellite Beach and a zip code of 32937.

That is a real semantic difference, not a performance note. The written query looks like a conjunction over one element, and the executed query is two independent existences over the collection. A document with Satellite Beach in one address and 32937 in a different one will match, and the LINQ expression does not convey that. Anyone writing a query against arrays of embedded objects needs to know this, and the documentation puts it in the open rather than leaving it to be discovered in production.

## The repository, and how to evaluate the library

The repository is a .NET solution with the usual structure: src/ for the library, test/ for the tests, examples/ for runnable samples, docs/, images/, a docker/ directory with a dockerfile, a Redis.OM.sln solution file, a global.json, a fetch-models.sh script and a CONTRIBUTING.md.

The examples directory is a good map of what the library is for. There is Redis.OM.BasicMatchingQueries, Redis.OM.CreateIndexStore, Redis.OM.DeleteDocument, Redis.OM.FullTextSearch, Redis.OM.LowLevelSearchIndex, Redis.OM.QueryingStringsWithinArrays, Redis.OM.RawQueryExample and Redis.OM.UpdatingDocuments.

That list covers the whole lifecycle and both levels of the API. CreateIndexStore is the index creation step, BasicMatchingQueries and QueryingStringsWithinArrays are the query side, FullTextSearch is the separate Searchable capability, RawQueryExample is the escape hatch when the fluent API does not reach, and LowLevelSearchIndex is for the case where you need to work with the underlying index directly. Update and Delete complete it.

For an evaluation, four things decide it.

First, whether you need Redis Stack. The library requires the search and JSON modules, and the failure when they are missing is a missing index rather than a clear error. The Docker image in the getting started section is the fastest way to get a working instance, and redis-insight on port 8001 is worth having open while you develop.

Second, the schema migration story. Limited support means you write the check-and-recreate step yourself, and you have to remember to run it. If your models change often, that is a recurring step in your deployment, and the IsIndexCurrent check is the tool for making it safe rather than automatic.

Third, the array-of-embedded-objects semantics, and this is the one that can produce wrong answers rather than slow ones. The gap between what the LINQ expression says and what the index does is documented, so it is knowable, but you have to know it before you write the query rather than after the data comes back wrong.

Fourth, the version. At 1.x with releases in 2026, the API is still moving, and the 1.0.1 to 1.2.0 to 1.3.0 sequence over a year suggests the surface has changed along the way. Pin the version and read the changelog between releases.

## Conclusion

Adopt Redis OM .NET if you are building a .NET application on Redis Stack and want a typed document model with indexes you do not have to maintain by hand, and if you are comfortable with Redis Stack rather than plain Redis. Do not expect it to remove the index maintenance question, because schema migration support is limited and you are expected to write the check-and-recreate step yourself. Before you commit, read the embedded document and array limitations carefully, since the gap between what a LINQ expression over an array of nested objects says and what the index actually does produces wrong results rather than slow ones, and plan for a Redis Stack instance with the search and JSON modules from the start, because the library cannot work without them.

## FAQ

### What does Redis OM .NET provide?

Four things: declarative object mapping for Redis objects, declarative secondary-index generation, fluent APIs for querying Redis, and fluent APIs for performing Redis aggregations. The primary language is C#, the licence is MIT, and the last push to the repository was on 2026-08-29 with v1.3.0 the most recent release.

### What does Redis OM .NET need to run?

A Redis instance with the appropriate modules, and the README says the quickest way to get one is the redis-stack Docker image, which also gives you the redis-insight GUI on port 8001 for visualising your data. A plain Redis will start and then fail at the first query, because the library needs the search and JSON capabilities.

### How do you model and index a document with Redis OM .NET?

Apply the Document attribute to the class, choosing a StorageType, and mark searchable properties with Indexed, optionally with Sortable set to true. Then create a RedisConnectionProvider with your connection string and call CreateIndex with the type. No index definition is written by hand, because it is derived from the attributes on the class.

### How does Redis OM .NET handle schema migration?

Support is limited. You can use GetIndexInfo and IsIndexCurrent on the RedisConnection to check whether the index definition in Redis still matches your current type, and use that to decide when to drop and re-create the index, as in the example that drops and recreates when IsIndexCurrent returns false. You write the drop-and-create step yourself.

### What is the limitation when indexing arrays of embedded objects?

Redis does not allow multiple predicates on a given document when fields are indexed within a collection of embedded objects. A query checking for a city and a zip code therefore matches a document that has the city in one address and the zip code in a different one, rather than one address with both.

## Sources

- [Issues](https://github.com/redis/redis-om-dotnet/issues)
- [License: MIT](https://github.com/redis/redis-om-dotnet/blob/main/LICENSE)
- [README](https://github.com/redis/redis-om-dotnet/blob/main/README.md)
- [redis/redis-om-dotnet on GitHub](https://github.com/redis/redis-om-dotnet)
- [Releases](https://github.com/redis/redis-om-dotnet/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/redis-redis-om-dotnet
