-
Notifications
You must be signed in to change notification settings - Fork 2
Features
Detailed reference for all emulator features beyond basic CRUD and queries.
For installation and quick-start examples, see Getting Started. For the full SQL query engine (125+ built-in functions), see SQL Queries. For behavioural differences from real Cosmos DB, see Known Limitations. For a side-by-side comparison with the official Microsoft emulator, see Feature Comparison.
Supports all five Cosmos DB patch operation types with deep nested path support.
Patch operations are atomic and executed sequentially, matching real Cosmos DB. If any operation in a batch fails, none of the operations are applied — the item remains unchanged.
// If the increment fails (e.g., /name is a string), the set is also rolled back
await container.PatchItemAsync<MyDocument>("id", new PartitionKey("pk"),
[
PatchOperation.Set("/name", "Changed"),
PatchOperation.Increment("/name", 1), // Fails — name is now a string
]);
// Item is unchanged — atomicity preserved| Operation | Description | Example |
|---|---|---|
Set |
Set a property value (creates if missing) | PatchOperation.Set("/status", "active") |
Replace |
Replace a property value (must exist) | PatchOperation.Replace("/name", "New Name") |
Add |
Add to array or set a new property | PatchOperation.Add("/tags/-", "new-tag") |
Remove |
Remove a property | PatchOperation.Remove("/tempField") |
Increment |
Increment a numeric value | PatchOperation.Increment("/version", 1) |
// Multiple operations in one call
var patchOps = new List<PatchOperation>
{
PatchOperation.Set("/status", "active"),
PatchOperation.Replace("/name", "Updated Name"),
PatchOperation.Add("/tags/-", "new-tag"), // Append to array
PatchOperation.Remove("/tempField"),
PatchOperation.Increment("/version", 1)
};
var response = await container.PatchItemAsync<MyDocument>(
"doc-id", new PartitionKey("pk"), patchOps);// Set a deeply nested property — intermediate objects are created automatically
PatchOperation.Set("/address/city/zipCode", "12345")// ETag-based: only patch if the document hasn't been modified
await container.PatchItemAsync<MyDocument>(
"id", new PartitionKey("pk"),
[PatchOperation.Set("/status", "done")],
new PatchItemRequestOptions { IfMatchEtag = etag });
// Filter predicate: only patch if a condition is met
await container.PatchItemAsync<MyDocument>(
"id", new PartitionKey("pk"),
[PatchOperation.Set("/name", "Patched")],
new PatchItemRequestOptions
{
FilterPredicate = "FROM c WHERE c.isActive = true"
});Patch operations targeting /id or the container's partition key path are rejected with CosmosException(HttpStatusCode.BadRequest), matching real Cosmos DB.
Computed property protection (v2.0.54+): Patch operations targeting computed property paths are also rejected with 400 BadRequest.
var response = await container.PatchItemStreamAsync(
"id", new PartitionKey("pk"), patchOps);
// Returns ResponseMessage with StatusCode instead of throwingAll stored items are automatically enriched with system metadata properties, matching real Cosmos DB behaviour:
| Property | Type | Description |
|---|---|---|
_ts |
long |
Unix epoch timestamp (seconds) of the last write |
_etag |
string |
Entity tag for optimistic concurrency |
_rid |
string |
Synthetic resource ID (v2.0.57+) |
_self |
string |
Synthetic self-link (v2.0.57+) |
_attachments |
string |
Synthetic attachments link (v2.0.57+) |
These properties are updated on every Create, Upsert, Replace, and Patch operation, and are available when reading items back:
// Read back system properties via JObject
var response = await container.ReadItemAsync<JObject>("id", new PartitionKey("pk"));
long timestamp = response.Resource["_ts"].Value<long>();
string etag = response.Resource["_etag"].ToString();
// Or map to your own type
public class MyDocument
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("_ts")] public long Timestamp { get; set; }
[JsonProperty("_etag")] public string ETag { get; set; }
}Atomic multi-operation execution with automatic rollback on failure.
var batch = container.CreateTransactionalBatch(new PartitionKey("pk"))
.CreateItem(new { id = "1", partitionKey = "pk", name = "Alice" })
.CreateItem(new { id = "2", partitionKey = "pk", name = "Bob" })
.ReplaceItem("3", updatedItem)
.PatchItem("4", new[] { PatchOperation.Set("/status", "done") })
.DeleteItem("5");
using var response = await batch.ExecuteAsync();
if (response.IsSuccessStatusCode)
{
// All operations succeeded atomically
var alice = response.GetOperationResultAtIndex<dynamic>(0);
}
else
{
// All operations rolled back — nothing was persisted
}-
CreateItem<T>/CreateItemStream -
UpsertItem<T>/UpsertItemStream -
ReplaceItem<T>/ReplaceItemStream DeleteItemReadItemPatchItem
- Maximum 100 operations per batch
- Maximum 2 MB total payload size
- All operations must target the same partition key
When any operation fails:
- All preceding operations are rolled back (data restored to pre-batch state)
- The failed operation gets its actual error status code (e.g. 404, 409)
- All preceding operations get
424 FailedDependency - All subsequent operations also get
424 FailedDependency
Supports the AllowBulkExecution = true pattern where many concurrent point operations are fired via Task.WhenAll. The SDK internally batches these into efficient service calls grouped by partition key.
// Set AllowBulkExecution on the client
using var cosmos = InMemoryCosmos.Create("container", "/partitionKey",
configureOptions: opts => opts.AllowBulkExecution = true);
// Fire hundreds of concurrent operations — the emulator handles them correctly
var tasks = items.Select(item =>
cosmos.Container.CreateItemAsync(item, new PartitionKey(item.PartitionKey)));
await Task.WhenAll(tasks);All point operations work correctly under bulk concurrency:
-
CreateItemAsync/CreateItemStreamAsync -
UpsertItemAsync/UpsertItemStreamAsync -
ReplaceItemAsync/ReplaceItemStreamAsync -
DeleteItemAsync/DeleteItemStreamAsync -
ReadItemAsync/ReadItemStreamAsync -
PatchItemAsync/PatchItemStreamAsync
-
Thread-safe storage — backed by
ConcurrentDictionary, all concurrent reads and writes are safe - Per-item mutation serialisation — All mutation operations (Create, Upsert, Delete, Replace, Patch) on the same item are serialised via an async mutex, preventing concurrent writes from interleaving dictionary updates (added in v2.0.136, extended to Create/Upsert in v2.0.137)
- Unique key enforcement — unique key policies are enforced atomically even under concurrent writes (no TOCTOU race conditions)
- ETag generation — each write generates a unique ETag
- Change feed recording — all concurrent writes are captured in the change feed
-
Conflict detection — duplicate
CreateItemAsynccalls with the same ID correctly return409 Conflict -
Conditional operations —
IfMatchEtag,IfNoneMatchEtag(including wildcard*) work correctly under concurrency - EnableContentResponseOnWrite — suppresses response body for all write operations (Create, Upsert, Replace, Patch)
-
CancellationToken — already-cancelled tokens throw
OperationCanceledExceptionimmediately -
Hierarchical partition keys — bulk operations work with composite partition keys via
PartitionKeyBuilder -
Filter predicate patches —
PatchItemRequestOptions.FilterPredicatecorrectly filters under bulk concurrency - TTL interaction — items with per-item or container-level TTL expire correctly after bulk creation
-
Stream error handling — stream methods return
ResponseMessagewith error status codes (404, 409, 412) instead of throwing - Document size limits — 2 MB limit enforced; oversized documents reject with HTTP 413
- DeleteAllItemsByPartitionKey — works correctly concurrent with bulk operations on different partitions
-
Hierarchical PK concurrency — concurrent writes with composite partition keys (
/tenantId+/userId) are fully thread-safe -
State persistence under concurrency —
ExportState()produces valid JSON even during concurrent writes - Batch isolation — transactional batches execute atomically (all-or-nothing) with snapshot rollback on failure. See Known Limitations for concurrent reader visibility during batch execution.
In real Cosmos DB, AllowBulkExecution causes the SDK to group concurrent operations into batched service calls for throughput optimisation. The in-memory emulator executes each operation individually — there is no batching layer. This has no functional impact; all operations produce identical results. The only difference is performance characteristics (the emulator does not simulate RU consumption or throughput limits).
Checkpoint-based (recommended for all versions + deletes):
The checkpoint-based iterator returns all versions of every change, including delete tombstones. This is the equivalent of Cosmos DB's AllVersionsAndDeletes mode.
long checkpoint = container.GetChangeFeedCheckpoint();
await container.CreateItemAsync(item1, new PartitionKey("pk"));
await container.UpsertItemAsync(item1Updated, new PartitionKey("pk"));
await container.DeleteItemAsync<MyDocument>("1", new PartitionKey("pk"));
var iterator = container.GetChangeFeedIterator<JObject>(checkpoint);
while (iterator.HasMoreResults)
{
var changes = await iterator.ReadNextAsync();
foreach (var change in changes)
{
if (change["_deleted"]?.Value<bool>() == true)
Console.WriteLine($"Deleted: {change["id"]}");
else
Console.WriteLine($"Changed: {change["id"]}");
}
}Delete tombstones contain { "id": "...", "<partitionKeyField>": "...", "_deleted": true, "_ts": ... }.
SDK-style start positions (Incremental / Latest Version):
The SDK-style iterators with ChangeFeedMode.Incremental return only the latest version of each item. Items whose last change is a delete are filtered out — matching real Cosmos DB Latest Version mode behaviour.
// All changes since container creation (latest version per item)
var iter1 = container.GetChangeFeedIterator<MyDoc>(
ChangeFeedStartFrom.Beginning(), ChangeFeedMode.Incremental);
// Only changes after this point
var iter2 = container.GetChangeFeedIterator<MyDoc>(
ChangeFeedStartFrom.Now(), ChangeFeedMode.Incremental);
// Changes after a specific timestamp
var iter3 = container.GetChangeFeedIterator<MyDoc>(
ChangeFeedStartFrom.Time(DateTime.UtcNow.AddMinutes(-5)),
ChangeFeedMode.Incremental);var processor = container
.GetChangeFeedProcessorBuilder<MyDocument>(
"myProcessor",
async (context, changes, ct) =>
{
foreach (var doc in changes)
Console.WriteLine($"Changed: {doc.Id}");
})
.Build();
await processor.StartAsync();
// ... changes will be delivered to the handler via polling ...
await processor.StopAsync();The processor polls the in-memory change feed every 50ms.
var processor = container
.GetChangeFeedProcessorBuilderWithManualCheckpoint<MyDocument>(
"myProcessor",
async (context, changes, checkpointAsync, ct) =>
{
foreach (var doc in changes)
Console.WriteLine($"Changed: {doc.Id}");
// Checkpoint only advances when you call checkpointAsync.
// If you don't call it, the same changes are redelivered on next poll.
await checkpointAsync();
})
.Build();
await processor.StartAsync();
// ... changes will be delivered to the handler via polling ...
await processor.StopAsync();Both typed (ChangeFeedHandlerWithManualCheckpoint<T>) and stream (ChangeFeedStreamHandlerWithManualCheckpoint) handlers are supported. If checkpointAsync is not called, the same batch of changes is redelivered on the next poll cycle, matching real Cosmos DB manual checkpoint semantics.
Note: Use
.WithInMemoryLeaseContainer()instead of.WithLeaseContainer()when building a change feed processor. This avoids the SDK's internal cast toContainerInternal.
var iterator = container.GetChangeFeedStreamIterator(
ChangeFeedStartFrom.Beginning(), ChangeFeedMode.Incremental);When an item is deleted, a tombstone entry is recorded in the change feed:
{ "id": "item-1", "partitionKey": "pk1", "_deleted": true, "_ts": 1743379200 }- Checkpoint-based iterator: Returns tombstones alongside creates/updates — use this to process deletes.
-
Incremental mode (
ChangeFeedMode.Incremental): Filters out items whose last change is a delete, matching real Cosmos DB Latest Version behaviour. - Change feed processors: Use the checkpoint-based iterator internally, so tombstones are delivered to the handler.
-
Failed writes are not recorded: Operations that throw (
CosmosException) — such as unique key violations, ETag conflicts, or failed conditional patches — do not produce a change feed entry -
ClearItems()resets the change feed: CallingClearItems()removes all entries from the change feed and resets the checkpoint to 0 -
PITR preserves the change feed log:
RestoreToPointInTime()replays the change feed to reconstruct container state, but does not delete change feed entries - Handler exceptions cause redelivery: If a processor handler throws, the checkpoint is not advanced and the same batch is redelivered on the next poll cycle
- All processor types deliver all versions: Unlike real Cosmos DB's latest-version processor, the in-memory processor delivers intermediate versions. Handlers should be designed for idempotent processing
- TTL eviction is silent: Items expired by TTL are lazily evicted without producing a change feed tombstone
-
Change feed size limiting: The change feed retains up to
MaxChangeFeedSizeentries (default 1000). When exceeded, the oldest entries are silently evicted. SetMaxChangeFeedSize = 0to disable eviction (unbounded growth). This limits memory usage during sustained write workloads -
Change feed items include
_tsand_etag: System properties are enriched before recording in the change feed -
FeedRange scoping works: Both typed and stream change feed iterators support
FeedRangefiltering viaChangeFeedStartFrom.Beginning(range),ChangeFeedStartFrom.Time(date, range), etc.
var response = await container.ReadItemAsync<MyDoc>("id", new PartitionKey("pk"));
var etag = response.ETag;
// Only replace if the document hasn't been modified
try
{
await container.ReplaceItemAsync(updatedDoc, "id", new PartitionKey("pk"),
new ItemRequestOptions { IfMatchEtag = etag });
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
{
// Another write happened — retry with fresh data
}try
{
await container.ReadItemAsync<MyDoc>("id", new PartitionKey("pk"),
new ItemRequestOptions { IfNoneMatchEtag = etag });
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotModified)
{
// Document hasn't changed — use cached version
}// IfMatch with "*" matches any existing document
new ItemRequestOptions { IfMatchEtag = "*" }
// IfNoneMatch with "*" means "only if document doesn't exist"
new ItemRequestOptions { IfNoneMatchEtag = "*" }// Container-level default (seconds)
container.DefaultTimeToLive = 3600; // items expire after 1 hour
// Enable TTL without a default expiry (only items with per-item _ttl expire)
container.DefaultTimeToLive = -1;
// Per-item override via _ttl property in JSON
var item = new { id = "1", partitionKey = "pk", _ttl = 7200, data = "..." };
await container.CreateItemAsync(item, new PartitionKey("pk"));
// Per-item _ttl = -1 means "never expire" even if container has a default TTL
var immortal = new { id = "2", partitionKey = "pk", _ttl = -1, data = "..." };
await container.CreateItemAsync(immortal, new PartitionKey("pk"));
// Disable TTL entirely (per-item _ttl is ignored)
container.DefaultTimeToLive = null;Semantics (matches real Cosmos DB):
Container DefaultTimeToLive
|
Item _ttl
|
Behaviour |
|---|---|---|
null |
any | Never expires (TTL disabled; per-item _ttl ignored) |
-1 |
missing | Never expires |
-1 |
-1 |
Never expires |
-1 |
N |
Expires after N seconds |
N |
missing | Expires after N seconds |
N |
-1 |
Never expires (item overrides container) |
N |
M |
Expires after M seconds (item overrides container) |
Validation (v2.0.53+):
- Setting
DefaultTimeToLive = 0throwsCosmosException(HttpStatusCode.BadRequest)— real Cosmos DB rejects zero as a TTL value. - Creating/upserting an item with
_ttl = 0throwsCosmosException(HttpStatusCode.BadRequest).
Eviction behaviour:
- Items are lazily evicted — expired items are removed the next time they are accessed via read, query, replace, patch, delete, or ReadMany.
- Upsert and Create on an expired item succeed (the expired item is evicted first).
- Replace/Upsert/Patch reset the TTL clock.
- TTL-triggered evictions do not produce change feed events (unlike real Cosmos DB in AllVersionsAndDeletes mode).
-
ReplaceContainerAsyncupdates the active TTL setting.
See Known Limitations for details on lazy eviction.
Register C# functions as stored procedure handlers:
container.RegisterStoredProcedure("bulkDelete", (pk, args) =>
{
// pk is the PartitionKey the sproc was called with
// args are the dynamic arguments passed to ExecuteStoredProcedureAsync
return JsonConvert.SerializeObject(new { deleted = 42 });
});
var response = await container.Scripts.ExecuteStoredProcedureAsync<string>(
"bulkDelete", new PartitionKey("pk"), new dynamic[] { "arg1" });
// Deregister when done
container.DeregisterStoredProcedure("bulkDelete");Validation (v2.0.53+): Executing a stored procedure that has not been registered (via RegisterStoredProcedure or CreateStoredProcedureAsync) returns CosmosException(HttpStatusCode.NotFound), matching real Cosmos DB.
Register C# pre-triggers and post-triggers that fire during create, upsert, and replace operations. Trigger invocation is controlled via ItemRequestOptions.PreTriggers / PostTriggers, matching the real SDK API.
Pre-triggers receive the incoming document as a JObject and return a (possibly modified) document:
container.RegisterTrigger("addTimestamp", TriggerType.Pre, TriggerOperation.Create,
preHandler: doc =>
{
doc["createdAt"] = DateTime.UtcNow.ToString("o");
return doc;
});
// Fire the trigger by specifying it in ItemRequestOptions
await container.CreateItemAsync(
new { id = "1", pk = "a" },
new PartitionKey("a"),
new ItemRequestOptions { PreTriggers = new List<string> { "addTimestamp" } });Post-triggers receive the committed document. If a post-trigger throws, the write is rolled back:
container.RegisterTrigger("audit", TriggerType.Post, TriggerOperation.Create,
postHandler: doc =>
{
// Perform side-effects (logging, auditing, etc.)
Console.WriteLine($"Created: {doc["id"]}");
});
await container.CreateItemAsync(
new { id = "1", pk = "a" },
new PartitionKey("a"),
new ItemRequestOptions { PostTriggers = new List<string> { "audit" } });Triggers only fire when the operation matches. Use TriggerOperation.All to fire on any write:
container.RegisterTrigger("validateAll", TriggerType.Pre, TriggerOperation.All,
preHandler: doc => { /* runs on create, upsert, and replace */ return doc; });container.DeregisterTrigger("addTimestamp");Install the CosmosDB.InMemoryEmulator.JsTriggers NuGet package to execute real Cosmos DB trigger JavaScript bodies. This uses the Jint interpreter and supports the standard server-side API: getContext(), getRequest()/getResponse(), getBody(), setBody().
using CosmosDB.InMemoryEmulator.JsTriggers;
// Enable JS trigger interpretation on a container
container.UseJsTriggers();
// Register a trigger with a JavaScript body via CreateTriggerAsync
await container.Scripts.CreateTriggerAsync(new TriggerProperties
{
Id = "addTimestamp",
TriggerType = TriggerType.Pre,
TriggerOperation = TriggerOperation.Create,
Body = @"
function addTimestamp() {
var context = getContext();
var request = context.getRequest();
var doc = request.getBody();
doc['timestamp'] = new Date().toISOString();
request.setBody(doc);
}"
});
// Fire it the same way as C# triggers
await container.CreateItemAsync(
new { id = "1", pk = "a" },
new PartitionKey("a"),
new ItemRequestOptions { PreTriggers = new List<string> { "addTimestamp" } });Priority: If both a C# handler (RegisterTrigger) and a JS body (CreateTriggerAsync) exist for the same trigger name, the C# handler takes priority.
Export and import container state for test data seeding, snapshots, or debugging. For the full guide including automatic persistence between test runs and point-in-time restore, see State Persistence.
// Export to string
string json = container.ExportState();
// Export to file
container.ExportStateToFile("test-data/snapshot.json");
// Import from string (replaces all data)
container.ImportState(json);
// Import from file
container.ImportStateFromFile("test-data/snapshot.json");
// Automatic persistence between test runs (v2.0.70+)
services.UseInMemoryCosmosDB(options =>
{
options.AddContainer("orders", "/customerId");
options.StatePersistenceDirectory = "./test-state"; // auto-save on dispose, auto-load on create
});
// Utilities
int count = container.ItemCount;
container.ClearItems(); // Remove everythingThe exported format is:
{
"items": [
{ "id": "1", "partitionKey": "pk1", "name": "Alice", ... },
{ "id": "2", "partitionKey": "pk2", "name": "Bob", ... }
]
}Restore a container to its state at any previous point in time. This replays the internal change feed up to the specified timestamp, reconstructing the exact state of every item:
// Create and modify data over time
await container.CreateItemAsync(item1, new PartitionKey("pk"));
var restorePoint = DateTimeOffset.UtcNow;
await container.CreateItemAsync(item2, new PartitionKey("pk"));
await container.DeleteItemAsync<MyDoc>("item1", new PartitionKey("pk"));
// Roll back to the restore point — item1 reappears, item2 is gone
container.RestoreToPointInTime(restorePoint);This is useful for:
- Debugging — inspect container state at a specific moment
- Test isolation — restore to a known baseline between test cases
- Simulating Cosmos DB continuous backup — test PITR-dependent recovery logic
Note: The restore replays the change feed, so it only works for changes made during the lifetime of the container instance. The change feed is not persisted across
ExportState/ImportStatecalls. Additionally, ifMaxChangeFeedSizeeviction has removed older entries, PITR may produce incomplete results. SetMaxChangeFeedSize = 0to disable eviction if you rely on PITR across large numbers of writes.
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");using var cosmos = InMemoryCosmos.Create("orders", new[] { "/tenantId", "/customerId" });When PartitionKey.None is used, the partition key value is extracted from the document body:
await container.CreateItemAsync(item, PartitionKey.None);
// PK value is extracted from item.partitionKey (or whatever the PK path points to)Note: The emulator treats
PartitionKey.NoneandPartitionKey.Nullidentically. See Known Limitations for details.
When querying with a prefix of a composite partition key (e.g. the first 1 or 2 components of a 3-level key), the query is automatically scoped to matching items. This works with all integration approaches (InMemoryCosmos, FakeCosmosHandler, Dependency Injection methods):
using var cosmos = InMemoryCosmos.Create("events",
new[] { "/tenantId", "/category", "/date" });
var cosmosContainer = cosmos.Container;
// Query by first PK component only
var prefixPk = new PartitionKeyBuilder().Add("tenant-1").Build();
var iterator = cosmosContainer.GetItemQueryIterator<Event>(
"SELECT * FROM c",
requestOptions: new QueryRequestOptions { PartitionKey = prefixPk });InMemoryCosmos handles PK prefix capturing automatically. See the Setup Guide — Hierarchical Partition Keys section for details.
Simulate multiple physical partitions and scope queries / change feeds to specific feed ranges.
By default, GetFeedRangesAsync() returns a single FeedRange covering the entire hash space. Set FeedRangeCount to simulate multiple partitions:
using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
configureContainer: c => c.FeedRangeCount = 4);
var ranges = await cosmos.Container.GetFeedRangesAsync();
// Returns 4 contiguous FeedRangeEpk ranges covering [min, max) of the hash spacePass a FeedRange to GetItemQueryIterator or GetItemQueryStreamIterator to scope results to a partition range:
var ranges = await container.GetFeedRangesAsync();
foreach (var range in ranges)
{
var iterator = container.GetItemQueryIterator<MyDoc>(
range, new QueryDefinition("SELECT * FROM c WHERE c.status = 'active'"));
while (iterator.HasMoreResults)
{
var page = await iterator.ReadNextAsync();
// Items scoped to this range only
}
}Pass a FeedRange to ChangeFeedStartFrom.Beginning() or ChangeFeedStartFrom.Now() to scope the change feed:
var ranges = await container.GetFeedRangesAsync();
foreach (var range in ranges)
{
var iterator = container.GetChangeFeedIterator<MyDoc>(
ChangeFeedStartFrom.Beginning(range),
ChangeFeedMode.Incremental);
while (iterator.HasMoreResults)
{
var page = await iterator.ReadNextAsync();
// Changes scoped to this range only
}
}Items are assigned to ranges using MurmurHash3 on the partition key value. The same algorithm is used by FakeCosmosHandler (via PartitionKeyRangeCount), so both approaches produce consistent partition assignments.
Tested edge cases include:
- Odd feed range counts (3, 5, 7) with contiguous boundaries
- Composite and numeric partition keys
- Null/missing partition key values (
PartitionKey.None) - Malformed FeedRange JSON (graceful fallback)
-
ChangeFeedStartFrom.Timewith FeedRange filtering - Pagination with FeedRange scoping
Batch-read multiple items by their (id, partitionKey) pairs:
var items = new List<(string, PartitionKey)>
{
("1", new PartitionKey("pk1")),
("2", new PartitionKey("pk1")),
("3", new PartitionKey("pk2")), // doesn't exist — silently omitted
};
var response = await container.ReadManyItemsAsync<MyDoc>(items);
// response.Resource contains only the found itemsA stream variant (ReadManyItemsStreamAsync) is also available, returning a ResponseMessage instead of deserialized objects.
All CRUD operations have stream variants that return ResponseMessage instead of throwing exceptions:
var response = await container.ReadItemStreamAsync("id", new PartitionKey("pk"));
if (response.StatusCode == HttpStatusCode.NotFound)
{
// Handle missing item without exception overhead
}Stream methods: CreateItemStreamAsync, ReadItemStreamAsync, UpsertItemStreamAsync, ReplaceItemStreamAsync, DeleteItemStreamAsync, PatchItemStreamAsync.
Stream query iterators (GetItemQueryStreamIterator) support continuation tokens and MaxItemCount pagination, matching the typed iterator behaviour.
Unique key constraints are enforced across all write operations:
using var cosmos = InMemoryCosmos.Create("users", "/pk",
configureContainer: c =>
{
c.UniqueKeyPolicy = new UniqueKeyPolicy
{
UniqueKeys = { new UniqueKey { Paths = { "/email" } } }
};
});
// First insert succeeds
await cosmos.Container.CreateItemAsync(
new { id = "1", pk = "a", email = "alice@example.com" },
new PartitionKey("a"));
// Duplicate email → CosmosException 409 Conflict
await cosmos.Container.CreateItemAsync(
new { id = "2", pk = "a", email = "alice@example.com" },
new PartitionKey("a")); // Throws!Unique keys are validated on:
-
CreateItemAsync/CreateItemStreamAsync -
UpsertItemAsync/UpsertItemStreamAsync -
ReplaceItemAsync/ReplaceItemStreamAsync PatchItemAsync
Full container lifecycle support: create, read, replace, delete — typed and stream variants.
// Read container properties
var response = await container.ReadContainerAsync();
// Replace container properties (Id must match)
await container.ReplaceContainerAsync(new ContainerProperties("my-container", "/pk") { DefaultTimeToLive = 3600 });
// Delete container
await container.DeleteContainerAsync();
// Delete all items in a partition
await container.DeleteAllItemsByPartitionKeyStreamAsync(new PartitionKey("pk1"));
// Throughput — persisted and round-tripped
await container.ReplaceThroughputAsync(1000);
var throughput = await container.ReadThroughputAsync(); // → 1000Container management features:
-
Throughput persistence —
ReplaceThroughputAsyncstores the value;ReadThroughputAsyncreturns it (defaults to 400 RU/s). Works on both containers and databases. -
Id validation on Replace —
ReplaceContainerAsyncandReplaceContainerStreamAsyncreject properties with mismatched container Ids (throwsCosmosExceptionwith 400 BadRequest / returns 400) -
All creation overloads —
CreateContainerAsync,CreateContainerIfNotExistsAsync,CreateContainerStreamAsyncwith optionalThroughputProperties -
Container query iterators —
GetContainerQueryIterator<T>andGetContainerQueryStreamIteratorwithQueryDefinitionor query text - Special character Ids — Container Ids with spaces, dots, underscores, parentheses, and Unicode characters are fully supported
- ClearItems() — Public method to reset a container's items, ETags, timestamps, and change feed without deleting the container
-
Idempotent delete —
DeleteContainerAsynccan be called multiple times without error -
Delete cascade (v2.0.53+) —
DeleteContainerAsyncclears stored procedures, UDFs, and triggers alongside items/ETags/timestamps/change feed -
Replace validation (v2.0.53+) —
ReplaceContainerAsyncrejects partition key path changes with 400 BadRequest, and preservesUniqueKeyPolicyif omitted from replacement properties -
Dispose tracking (v2.0.53+) — The emulator tracks disposal state; calling
CreateDatabaseAsync,CreateDatabaseIfNotExistsAsync,GetDatabase, orGetContainerafterDispose()throwsObjectDisposedException -
Database property (v2.0.54+) —
Container.Databasereturns a cached instance with the correct parent database Id. When created via a database,container.Database.Idreturns the database name - Pre-trigger size validation (v2.0.55+) — Document size is re-validated after pre-trigger execution. Pre-triggers that inflate documents past 2MB are rejected with 413, matching real Cosmos DB
-
Stream ErrorMessage (v2.0.55+) —
ResponseMessage.ErrorMessageis now set for all error status codes (≥400), providing human-readable error descriptions
Note: Database.GetContainer("name") auto-creates the container for test convenience (real SDK returns a proxy where the first data operation throws 404). Database and container query iterators return all items in a single page — MaxItemCount paging and WHERE clause filtering are not supported for metadata queries.
var queryable = container.GetItemLinqQueryable<MyDoc>(allowSynchronousQueryExecution: true);
// Standard LINQ operators
var results = queryable
.Where(d => d.Status == "active")
.OrderBy(d => d.Name)
.Take(10)
.ToList();
// With partition key filter
var results2 = container
.GetItemLinqQueryable<MyDoc>(true,
requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("pk1") })
.Where(d => d.Age > 30)
.ToList();For .ToFeedIterator() support in tests, see How Does ToFeedIterator() Work? and the Setup Guide.
All four full-text search functions are implemented with case-insensitive substring matching as an approximation of the real Cosmos DB NLP-based full-text indexing engine.
Note: Real Cosmos DB requires a full-text indexing policy on the container. The emulator skips this validation — queries work on any container without configuration.
Returns true if the field contains the search term (case-insensitive):
SELECT * FROM c WHERE FULLTEXTCONTAINS(c.description, 'database')Returns true if the field contains all of the search terms:
SELECT * FROM c WHERE FULLTEXTCONTAINSALL(c.text, 'cosmos', 'database', 'azure')Returns true if the field contains any of the search terms:
SELECT * FROM c WHERE FULLTEXTCONTAINSANY(c.text, 'cosmos', 'sql', 'mongo')FULLTEXTSCORE returns a relevance score for the given search terms. Use with ORDER BY RANK to sort results by relevance (highest score first):
-- Sort by relevance
SELECT * FROM c ORDER BY RANK FullTextScore(c.text, ['database', 'cosmos'])
-- Combine with filtering and projection
SELECT c.id, FullTextScore(c.text, ['database', 'cosmos']) AS score
FROM c
WHERE c.category = 'docs'
ORDER BY RANK FullTextScore(c.text, ['database', 'cosmos'])| Aspect | Real Cosmos DB | InMemoryEmulator |
|---|---|---|
| Indexing policy | Required (FullTextPolicy + FullTextIndexes) | Not required |
| Text analysis | Tokenization, stemming, stop-word removal | Case-insensitive substring matching |
| Scoring algorithm | BM25 (TF-IDF + length normalization) | Naive term-frequency count |
| Stemming | "running" matches "runs", "ran" | Literal matching only |
For the complete list of full-text search behavioural differences, see Known Limitations.
VECTORDISTANCE computes the distance between two vectors using cosine similarity, dot product, or Euclidean distance. Works in SELECT, WHERE, and ORDER BY clauses.
-- Find the 10 most similar items by cosine similarity
SELECT TOP 10 c.id, VectorDistance(c.embedding, [0.1, 0.2, 0.3]) AS score
FROM c
ORDER BY VectorDistance(c.embedding, [0.1, 0.2, 0.3])
-- Filter by similarity threshold
SELECT * FROM c
WHERE VectorDistance(c.embedding, [0.1, 0.2, 0.3], false, {distanceFunction: 'cosine'}) < 0.5| Distance Function | Description |
|---|---|
cosine |
Cosine similarity (default) — `dot(a,b) / ( |
dotproduct |
Dot product — higher values indicate greater similarity |
euclidean |
Euclidean (L2) distance |
| Aspect | Real Cosmos DB | InMemoryEmulator |
|---|---|---|
| Vector index policy | Required for approximate nearest neighbour (ANN) | Not required — always brute-force exact computation |
| Zero-magnitude vectors | Returns no score (null-like) | Returns null for cosine (undefined); dot product returns 0 |
| Dimensionality limits | flat: 505, quantizedFlat/diskANN: 4096 | No limit (tested up to 2000) |
| Additional options |
dataType, searchListSizeMultiplier, filterPriority affect index behaviour |
Accepted but ignored |
| Index types | flat, quantizedFlat, diskANN | Not applicable |
| Performance | ANN indexing for sub-linear search | Linear scan (suitable for test data volumes) |
| Unknown distance function | Rejected with error |
CosmosException(BadRequest)
|
| Extra args (>4) | Rejected at query compilation |
CosmosException(BadRequest)
|
| Infinity/NaN overflow | Unlikely (bounded dimensions) | Returns null for non-finite results |
- CRUD interaction: VectorDistance queries reflect upserts, patches, and deletes immediately
-
Change feed: Vector field updates appear in
GetChangeFeedIteratorresults -
Stream API:
GetItemQueryStreamIteratorreturns valid JSON with vector scores - Cross-partition: Works with both single and hierarchical partition keys
-
Arithmetic expressions:
VectorDistance(...) * 100,VectorDistance(...) + 5,ABS(VectorDistance(...)),IIF(VectorDistance(...) > threshold, ...)all work - Concurrent reads: Thread-safe for parallel vector queries
For the full list of supported SQL functions, see SQL Queries.
All six Cosmos DB geospatial functions are implemented with real geometric calculations:
| Function | Implementation |
|---|---|
ST_DISTANCE(a, b) |
Haversine formula (metres) |
ST_WITHIN(point, region) |
Point-in-polygon (ray casting) and point-in-circle (haversine radius) |
ST_INTERSECTS(geo1, geo2) |
Point-point, point-polygon, and polygon-polygon overlap detection |
ST_ISVALID(geojson) |
Full GeoJSON structure validation (Point, Polygon, LineString, MultiPoint) |
ST_ISVALIDDETAILED(geojson) |
Returns { valid, reason } with specific validation error messages |
ST_AREA(polygon) |
Spherical excess formula |
-- Find locations within 5 km
SELECT * FROM c
WHERE ST_DISTANCE(c.location, {'type': 'Point', 'coordinates': [-122.4, 37.8]}) < 5000
-- Check if a point is within a polygon
SELECT * FROM c
WHERE ST_WITHIN(c.location, {
'type': 'Polygon',
'coordinates': [[[-122.5, 37.7], [-122.3, 37.7], [-122.3, 37.9], [-122.5, 37.9], [-122.5, 37.7]]]
})Note: Results are geometrically correct but may differ slightly from Cosmos DB's exact values due to different calculation methods. See Known Limitations for details.
The emulator enforces the same 2 MB maximum document size as real Cosmos DB. Documents exceeding this limit are rejected with a RequestEntityTooLarge (413) status code on create, upsert, replace, and patch operations.
Transactional batches also enforce the 2 MB total payload size limit across all operations in the batch.
In-memory stub implementation of Cosmos DB's user and permission management. CRUD operations work and return realistic responses with synthetic metadata. No actual authorization is enforced — all data operations succeed regardless of permission settings.
var db = client.GetDatabase("myDb");
// Create a user
var response = await db.CreateUserAsync("user1");
// response.StatusCode == HttpStatusCode.Created
// Upsert (create or update)
await db.UpsertUserAsync("user1"); // 200 OK if exists, 201 Created if new
// Get user proxy
var user = db.GetUser("user1");
// Query all users
var iterator = db.GetUserQueryIterator<UserProperties>();var user = db.GetUser("user1");
// Read
var readResponse = await user.ReadAsync();
// readResponse.Resource.Id == "user1"
// Replace
await user.ReplaceAsync(new UserProperties("user1-renamed"));
// Delete
await user.DeleteAsync(); // 204 NoContent, removed from databasevar user = db.GetUser("user1");
var container = db.GetContainer("myContainer");
// Create permission
var permProps = new PermissionProperties("perm1", PermissionMode.All, container);
var response = await user.CreatePermissionAsync(permProps);
// response.Resource.Token contains a synthetic token string
// Upsert permission
await user.UpsertPermissionAsync(new PermissionProperties("perm1", PermissionMode.Read, container));
// Read permission
var perm = user.GetPermission("perm1");
var readResponse = await perm.ReadAsync();
// Replace permission
await perm.ReplaceAsync(new PermissionProperties("perm1", PermissionMode.All, container));
// Delete permission
await perm.DeleteAsync();
// Query all permissions for a user
var iterator = user.GetPermissionQueryIterator<PermissionProperties>();| Aspect | Real Cosmos DB | InMemoryEmulator |
|---|---|---|
| Permission tokens | Cryptographic, time-limited resource tokens | Synthetic placeholders (type=resource&ver=1&sig=stub_{id}) |
| Token expiry | Enforced (tokenExpiryInSeconds) |
Accepted but not enforced |
| Authorization | Operations scoped by permission mode | No authorization — all operations succeed |
| Resource scoping | Tokens grant access to specific containers/items | No scoping — permissions are metadata only |
For the full list of behavioural differences, see Known Limitations.
Computed properties are virtual top-level properties defined on a container with a Name and a Query. They are not persisted on documents — instead, queries can reference them like regular properties in SELECT, WHERE, ORDER BY, and GROUP BY.
using var cosmos = InMemoryCosmos.Create("myContainer", "/pk",
configureContainer: c =>
{
c.ComputedProperties = new Collection<ComputedProperty>
{
new ComputedProperty
{
Name = "cp_lowerName",
Query = "SELECT VALUE LOWER(c.name) FROM c"
},
new ComputedProperty
{
Name = "cp_discountedPrice",
Query = "SELECT VALUE c.price * 0.8 FROM c"
}
};
});// SELECT projection
var q1 = "SELECT c.cp_lowerName FROM c";
// WHERE filtering
var q2 = "SELECT c.id FROM c WHERE c.cp_lowerName = 'alice'";
// ORDER BY
var q3 = "SELECT c.id FROM c ORDER BY c.cp_lowerName ASC";
// GROUP BY
var q4 = "SELECT c.cp_lowerName, COUNT(1) AS cnt FROM c GROUP BY c.cp_lowerName";var readResp = await container.ReadContainerAsync();
var updatedProps = readResp.Resource;
updatedProps.ComputedProperties = new Collection<ComputedProperty>
{
new ComputedProperty { Name = "cp_upperName", Query = "SELECT VALUE UPPER(c.name) FROM c" }
};
await container.ReplaceContainerAsync(updatedProps);-
SELECT *does not include computed properties (matching real Cosmos DB) - Computed properties are evaluated at query time using the emulator's SQL expression engine
- All 125+ built-in SQL functions are available in computed property queries
- Zero performance overhead for containers without computed properties
- Point reads (
ReadItemAsync,ReadItemStreamAsync) do not include computed properties -
ReadManyItemsAsync/ReadManyItemsStreamAsyncdo not include computed properties - Change feed results do not include computed properties
- Transactional batch reads do not include computed properties
- Works with:
SELECT,WHERE,ORDER BY,GROUP BY,DISTINCT,TOP,OFFSET/LIMIT,VALUE, aggregates, aliases, boolean logic, arithmetic,IN,BETWEEN, parameterized queries - Expression types: string functions, math functions, nested property access, type-check functions, array functions, IIF conditionals, coalesce (
??), CONCAT, StringSplit, TimestampToDateTime, ToString - CPs re-evaluate correctly after document updates (upsert, replace)
-
ReplaceContainerAsync/ReplaceContainerStreamAsyncinvalidate CP cache — old CPs removed, new CPs active - Cross-partition queries work correctly with CPs
- Multiple CPs can be used together in a single query (SELECT, WHERE, ORDER BY)
- Non-
cFROM aliases (e.g.FROM root) are supported in CP definitions - Creating containers with CPs via
Database.CreateContainerAsyncis supported - LINQ queries (
GetItemLinqQueryable) do not include computed properties (known limitation — LINQ bypasses the SQL engine) - See Known Limitations for undefined propagation, validation gaps, and SELECT c.* issues
- Maximum 20 computed properties per container (matching real Cosmos DB)
- Reserved system property names rejected:
id,_rid,_ts,_etag,_self,_attachments -
SELECT VALUE required (v2.0.54+): CP queries must use
SELECT VALUEsyntax - Prohibited clauses (v2.0.54+): WHERE, ORDER BY, GROUP BY, TOP, DISTINCT, OFFSET, LIMIT, JOIN rejected in CP queries
- Cross-CP references (v2.0.54+): A computed property cannot reference another computed property
- Validation runs on both container construction and
ReplaceContainerAsync
Getting Started
Integration & Dependency Injection
Data Management
Reference
Help