CRUD Operations
Document Types
Section titled “Document Types”Every document type must have a public Id property of type Guid, int, long, or string. The Id is stored in both the database Id column and inside the JSON blob, so query results always include it.
public class User{ public string Id { get; set; } = ""; public string Name { get; set; } = ""; public int Age { get; set; } public string? Email { get; set; }}Auto-generation rules
Section titled “Auto-generation rules”| Id CLR Type | Default Value | Auto-Gen Strategy |
|---|---|---|
Guid | Guid.Empty | Guid.NewGuid() |
string | null or "" | Throws InvalidOperationException |
int | 0 | MAX(CAST(Id AS INTEGER)) + 1 per TypeName |
long | 0 | MAX(CAST(Id AS INTEGER)) + 1 per TypeName |
When Insert is called with a default Id, the store auto-generates one for Guid, int, and long types and writes it back to the object. For string Ids, a default value throws an InvalidOperationException — you must always supply an explicit Id. When a non-default Id is provided, it is used as-is.
Insert a document
Section titled “Insert a document”// Auto-generated ID (Guid) — written back to the objectpublic class UserGuid{ public Guid Id { get; set; } public string Name { get; set; } = ""; public int Age { get; set; }}
var user = new UserGuid { Name = "Alice", Age = 25 };await store.Insert(user);// user.Id is now populated
// Explicit ID (string)await store.Insert(new User { Id = "user-1", Name = "Alice", Age = 25 });Batch insert
Section titled “Batch insert”BatchInsert inserts multiple documents in a single transaction with prepared command reuse for optimal performance. Returns the count inserted. If any document fails (e.g. duplicate Id), the entire batch is rolled back. Auto-generates IDs for Guid, int, and long Id types.
var users = Enumerable.Range(1, 1000).Select(i => new User{ Id = $"user-{i}", Name = $"User {i}", Age = 20 + (i % 50)});
var count = await store.BatchInsert(users); // single transaction, prepared command reused
// Inside a transaction — uses the existing transaction (no nesting)await store.RunInTransaction(async tx =>{ await tx.BatchInsert(moreUsers); await tx.Insert(singleUser); // All committed or rolled back together});Upsert with JSON Merge Patch
Section titled “Upsert with JSON Merge Patch”Upsert uses JSON merge patch (RFC 7396) to deep-merge a partial patch into an existing document. If the document doesn’t exist, it is inserted as-is. Unlike Update, which replaces the entire document, Upsert only overwrites the fields present in the patch. The document must have a non-default Id.
// Insert a full documentawait store.Insert(new User { Id = "user-1", Name = "Alice", Age = 25, Email = "alice@test.com" });
// Merge patch — only update Name and Age, preserve Emailawait store.Upsert(new User { Id = "user-1", Name = "Alice", Age = 30 });
var user = await store.Get<User>("user-1");// user.Name == "Alice", user.Age == 30, user.Email == "alice@test.com" (preserved)How it works:
- On insert (new ID): the patch is stored as the full document.
- On conflict (existing ID): the provider’s JSON merge function deep-merges the patch into the stored JSON. Objects are recursively merged; scalars and arrays are replaced.
- Null properties are excluded from the patch automatically. In C#, unset nullable properties (e.g.
string? Email) serialize asnull, which would remove the key under RFC 7396. The library strips these so that unset fields are preserved rather than deleted.
Update a single property (SetProperty)
Section titled “Update a single property (SetProperty)”SetProperty updates a single scalar field in-place using the provider’s JSON set function — no deserialization, no full document replacement. Returns true if the document was found and updated, false if not found. The id parameter accepts Guid, int, long, or string.
// Update a scalar fieldawait store.SetProperty<User>("user-1", u => u.Age, 31);
// Update a string fieldawait store.SetProperty<User>("user-1", u => u.Email, "newemail@test.com");
// Set a field to nullawait store.SetProperty<User>("user-1", u => u.Email, null);
// Nested propertyawait store.SetProperty<Order>("order-1", o => o.ShippingAddress.City, "Portland");
// Check if document existedbool updated = await store.SetProperty<User>("user-1", u => u.Age, 31);How it works: The expression u => u.Age is resolved to the JSON path $.age (respecting [JsonPropertyName] attributes and naming policies). The generated SQL varies by provider — for example, SQLite uses json_set(), SQL Server uses JSON_MODIFY(), etc.
Supported value types: string, int, long, double, float, decimal, bool, and null. To replace a collection or nested object, use Update (full replacement) or Upsert (merge patch).
Remove a single property (RemoveProperty)
Section titled “Remove a single property (RemoveProperty)”RemoveProperty strips a field from the stored JSON using the provider’s JSON remove function. Returns true if the document was found and updated, false if not found. The removed field will have its C# default value on next read. The id parameter accepts Guid, int, long, or string.
// Remove a nullable fieldawait store.RemoveProperty<User>("user-1", u => u.Email);
// Remove a nested propertyawait store.RemoveProperty<Order>("order-1", o => o.ShippingAddress.City);
// Remove a collection property (removes the entire array)await store.RemoveProperty<Order>("order-1", o => o.Tags);Unlike SetProperty, RemoveProperty works on any property type — scalar, nested object, or collection — because it simply removes the key from the JSON.
Choosing an update strategy
Section titled “Choosing an update strategy”| Operation | Use when | Scope | Collections |
|---|---|---|---|
SetProperty | Changing one scalar field | Single field via JSON set | Scalar values only |
RemoveProperty | Stripping a field from the document | Single field via JSON remove | Any property type |
Upsert | Patching multiple fields at once | Deep merge via JSON merge patch | Replaces arrays (RFC 7396) |
Update | Replacing the entire document | Full replacement | Full control |
Insert | Inserting a new document | Strict insert, throws on duplicate | Throws for string default Ids |
BatchInsert | Inserting many documents efficiently | Single transaction, prepared command | Auto-generates IDs; atomic rollback |
GetDiff | Diffing local changes vs stored state | Read-only; returns RFC 6902 patch | Deep nested diff; arrays replaced as whole |
Optimistic Concurrency (Row Versioning)
Section titled “Optimistic Concurrency (Row Versioning)”Map a version property on your document type for automatic optimistic concurrency checks. The version is stored inside the JSON blob — no schema or table changes required. Works across all providers.
Configuration
Section titled “Configuration”// Expression-based (reflection)var store = new DocumentStore(new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")}.MapVersionProperty<Order>(o => o.RowVersion));
// AOT-safe overloadvar store = new DocumentStore(new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")}.MapVersionProperty<Order>("RowVersion", o => o.RowVersion, (o, v) => o.RowVersion = v));All provider options classes support MapVersionProperty: DocumentStoreOptions, LiteDbDocumentStoreOptions, CosmosDbDocumentStoreOptions, and IndexedDbDocumentStoreOptions.
How it works
Section titled “How it works”| Operation | Behavior |
|---|---|
Insert | Version is set to 1 before serialization |
Update | Reads the expected version from the object, checks it against the stored version, then increments. Throws ConcurrencyException on mismatch |
Upsert | Insert path sets version to 1. Update path checks and increments (only when the existing version > 0) |
BatchInsert | Version is set to 1 for each document |
Example
Section titled “Example”public class Order{ public string Id { get; set; } = ""; public string Status { get; set; } = ""; public int RowVersion { get; set; }}
// Insert — RowVersion is set to 1var order = new Order { Id = "ord-1", Status = "Pending" };await store.Insert(order);// order.RowVersion == 1
// Update — RowVersion is checked and incrementedorder.Status = "Shipped";await store.Update(order);// order.RowVersion == 2
// Concurrent update — throws ConcurrencyExceptionvar staleOrder = new Order { Id = "ord-1", Status = "Cancelled", RowVersion = 1 };await store.Update(staleOrder); // throws ConcurrencyExceptionConcurrencyException
Section titled “ConcurrencyException”ConcurrencyException provides diagnostic properties:
| Property | Description |
|---|---|
TypeName | The document type name |
DocumentId | The document Id |
ExpectedVersion | The version the caller expected |
ActualVersion | The version found in the store (when available) |
try{ await store.Update(staleOrder);}catch (ConcurrencyException ex){ Console.WriteLine($"Conflict on {ex.TypeName} {ex.DocumentId}: expected v{ex.ExpectedVersion}, found v{ex.ActualVersion}");}Get a document
Section titled “Get a document”The id parameter accepts Guid, int, long, or string. Passing an unsupported type throws ArgumentException.
var user = await store.Get<User>("user-1");
// Guid, int, and long Ids work directly — no ToString() neededvar item = await store.Get<GuidIdModel>(myGuid);var order = await store.Get<IntIdModel>(42);Diff against stored document (GetDiff)
Section titled “Diff against stored document (GetDiff)”Compare a modified object against the stored document and get an RFC 6902 JsonPatchDocument<T> describing the differences. Returns null if no document with that ID exists.
var proposed = new Order{ Id = "ord-1", CustomerName = "Alice", Status = "Delivered", ShippingAddress = new() { City = "Seattle", State = "WA" }, Lines = [new() { ProductName = "Widget", Quantity = 10, UnitPrice = 8.99m }], Tags = ["priority", "expedited"]};
var patch = await store.GetDiff("ord-1", proposed);// patch.Operations:// Replace /status → Delivered// Replace /shippingAddress/city → Seattle// Replace /shippingAddress/state → WA// Replace /lines → [...]// Replace /tags → [...]
// Apply the patch to any instance of the same typevar current = await store.Get<Order>("ord-1");patch!.ApplyTo(current!);The diff is deep — nested objects produce individual property-level operations (e.g. /shippingAddress/city), while arrays and collections are replaced as a whole. Works with table-per-type, custom Id, and inside transactions.
Get all documents of a type
Section titled “Get all documents of a type”var users = await store.Query<User>().ToList();Remove a document
Section titled “Remove a document”The id parameter accepts Guid, int, long, or string. Passing an unsupported type throws ArgumentException.
// By IDbool deleted = await store.Remove<User>("user-1");bool removed = await store.Remove<GuidIdModel>(myGuid);Remove documents matching a predicate
Section titled “Remove documents matching a predicate”// Returns number of deleted rowsint deleted = await store.Query<User>().Where(u => u.Age < 18).ExecuteDelete();Update documents matching a predicate
Section titled “Update documents matching a predicate”// Update a property on matching docs — returns number of updated rowsint updated = await store.Query<User>() .Where(u => u.Age < 18) .ExecuteUpdate(u => u.Age, 18);See Querying for more examples of bulk delete and update with expressions.
Clear all documents of a type
Section titled “Clear all documents of a type”int deletedCount = await store.Clear<User>();Backup
Section titled “Backup”Creates a hot backup of the database to a file. The store remains fully usable during the backup. Only available on concrete store types — not on the IDocumentStore interface.
| Store | Behavior |
|---|---|
SqliteDocumentStore | Uses the SQLite Online Backup API |
SqlCipherDocumentStore | Backup is automatically encrypted with the same password |
LiteDbDocumentStore | Requires a file-based connection string with a Filename parameter |
// SQLitevar sqliteStore = new SqliteDocumentStore("Data Source=mydata.db");await sqliteStore.Backup("/path/to/backup.db");
// SQLCipher — backup encrypted with same passwordvar cipherStore = new SqlCipherDocumentStore("encrypted.db", "mySecretKey");await cipherStore.Backup("/path/to/backup.db");
// LiteDBvar liteStore = new LiteDbDocumentStore(new LiteDbDocumentStoreOptions{ ConnectionString = "Filename=mydata.db"});await liteStore.Backup("/path/to/backup.db");ClearAllAsync (SQLite only)
Section titled “ClearAllAsync (SQLite only)”Deletes all documents across all tables in the SQLite database, including spatial sidecar tables. Only available on SqliteDocumentStore.
var sqliteStore = new SqliteDocumentStore("Data Source=mydata.db");await sqliteStore.ClearAllAsync();