Repository navigation
Whats New In 3.0
Version 3.0.0 introduces a unified entry-point API (InMemoryCosmos) that replaces the manual FakeCosmosHandler wiring patterns, adds dynamic container management, and adds multi-database support — all while keeping existing DI and direct InMemoryContainer usage completely unchanged.
Upgrading from 2.x? Jump to Migration Guide below, or see the dedicated Migration Guide page.
A single static class replaces the three-step InMemoryContainer → FakeCosmosHandler → CosmosClient dance. Full SDK fidelity, zero production code changes, one line of setup.
Single container:
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
await cosmos.Container.CreateItemAsync(order, new PartitionKey(order.CustomerId));
var response = await cosmos.Container.ReadItemAsync<Order>("1", new PartitionKey("c1"));Multi-container:
using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.AddContainer("products", "/categoryId")
.Build();
var orders = cosmos.Containers["orders"];
var products = cosmos.Containers["products"];The result object (InMemoryCosmosResult) exposes three tiers of access:
| Tier | Properties | When you need it |
|---|---|---|
| Production-like |
Client, Container, Containers
|
Every test — CRUD, queries, LINQ |
| Test setup |
SetupContainer() → IContainerTestSetup
|
Registering sprocs/UDFs/triggers, seeding data, state import/export, clearing items |
| Fault injection |
Handler, Handlers, GetHandler(), SetFaultInjector()
|
Simulating 429 throttling, 503 errors, timeouts; inspecting query logs |
Production startup code that calls CreateContainerAsync / DeleteContainerAsync now works without pre-registration:
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
// Production code creates a new container at runtime
var db = cosmos.Client.GetDatabase("default");
await db.CreateContainerAsync(new ContainerProperties("shipments", "/orderId"));
// The new container is immediately available
var shipments = cosmos.Client.GetContainer("default", "shipments");
await shipments.CreateItemAsync(
new { id = "1", orderId = "o1" }, new PartitionKey("o1"));Supported operations:
-
CreateContainerAsync/CreateContainerIfNotExistsAsync DeleteContainerAsync-
ReadContainerAsync/ReplaceContainerAsync -
GetContainerQueryIterator(list containers) -
CreateDatabaseAsync/CreateDatabaseIfNotExistsAsync -
ReadDatabaseAsync/DeleteDatabaseAsync
Dynamically created containers are fully functional — they appear in cosmos.Containers, work with cosmos.SetupContainer() and cosmos.GetHandler(), and support fault injection.
For codebases where a single CosmosClient talks to multiple databases with overlapping container names:
using var cosmos = InMemoryCosmos.Builder()
.AddDatabase("users-db", db => {
db.AddContainer("events", "/userId");
db.AddContainer("profiles", "/userId");
})
.AddDatabase("orders-db", db => {
db.AddContainer("events", "/orderId");
db.AddContainer("products", "/categoryId");
})
.Build();
// Database-scoped access — no ambiguity
var userEvents = cosmos.Database("users-db").Containers["events"];
var orderEvents = cosmos.Database("orders-db").Containers["events"];
// Fault injection scoped to one database
cosmos.Database("orders-db").SetFaultInjector(req =>
new HttpResponseMessage((HttpStatusCode)503));When container names are globally unique, flat access still works:
using var cosmos = InMemoryCosmos.Builder()
.AddDatabase("users-db", db => db.AddContainer("profiles", "/userId"))
.AddDatabase("orders-db", db => db.AddContainer("orders", "/orderId"))
.Build();
var profiles = cosmos.Containers["profiles"]; // works — unique name
var orders = cosmos.Containers["orders"]; // works — unique nameWhen names collide, flat access throws with clear guidance:
KeyNotFoundException: Container 'events' exists in multiple databases: orders-db, users-db.
Use cosmos.Database("users-db").Containers["events"] instead.
See Multi-Database for the full guide.
A focused interface that filters the InMemoryContainer surface to test-only operations — cleaner IntelliSense, clearer intent:
public interface IContainerTestSetup
{
// C# delegate registrations
void RegisterStoredProcedure(string id, Func<PartitionKey, dynamic[], string> handler);
void RegisterUdf(string name, Func<object[], object> handler);
void RegisterTrigger(string id, TriggerType type, TriggerOperation operation,
Func<JObject, JObject> handler);
// JS body registrations (requires JsTriggers package — throws helpful error if missing)
void RegisterStoredProcedure(string id, string jsBody);
void RegisterUdf(string name, string jsBody);
void RegisterTrigger(string id, TriggerType type, TriggerOperation operation, string jsBody);
// State management
string ExportState();
void ExportStateToFile(string path);
void ImportState(string json);
void ImportStateFromFile(string path);
void RestoreToPointInTime(DateTimeOffset timestamp);
// Reset
void ClearItems();
}Access via cosmos.SetupContainer():
cosmos.SetupContainer().RegisterUdf("discount", args => (double)args[0] * 0.9);
cosmos.ImportState(File.ReadAllText("seed-data.json"));
cosmos.ClearItems();
// Multi-container: specify by name
cosmos.ClearItems("orders");
cosmos.ImportState(productsSeedJson, "products");The builder supports handler wrapping and custom CosmosClientOptions — no more manual HTTP pipeline wiring:
using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.WrapHandler(h => new CosmosTrackingMessageHandler(trackingOptions, h))
.ConfigureOptions(opts => opts.Serializer = new MyCamelCaseSerializer())
.Build();Multiple WrapHandler and ConfigureOptions calls compose in order:
.WrapHandler(a) // a wraps the base handler
.WrapHandler(b) // b wraps a
.ConfigureOptions(x) // x runs first
.ConfigureOptions(y) // y runs second| Member | Description |
|---|---|
DefaultDatabaseName |
"default" — the database used by Create() and AddContainer()
|
Create(containerName, partitionKeyPath, ...) |
Single-container one-liner |
Create(containerName, partitionKeyPaths, ...) |
Single-container with hierarchical PK |
Builder() |
Returns InMemoryCosmosBuilder for multi-container/advanced setups |
| Method | Description |
|---|---|
AddContainer(name, pkPath, configure?) |
Add a container to the default database |
AddContainer(name, pkPaths, configure?) |
Add a container with hierarchical partition key |
AddDatabase(dbName, configure) |
Add a named database with its own containers |
WrapHandler(wrapper) |
Insert HTTP DelegatingHandler middleware (logging, tracking, metrics) |
ConfigureOptions(configure) |
Configure CosmosClientOptions (serializer, timeouts, etc.) |
Build() |
Build the result — can be called multiple times for independent results |
| Member | Description |
|---|---|
Client |
The CosmosClient backed by in-memory handlers |
Container |
SDK Container (single-container only — throws if multi) |
Containers |
All SDK containers by name |
SetupContainer(name?) |
Returns IContainerTestSetup (registration) |
ExportState(name?) |
Export container state as JSON |
ImportState(json, name?) |
Import container state from JSON |
ImportStateFromFile(filePath, name?) |
Import container state from a file |
ExportStateToFile(path, name?) |
Export container state to a file |
RestoreToPointInTime(timestamp, name?) |
Restore container to a point in time |
ClearItems(name?) |
Remove all items from the container |
Handler |
FakeCosmosHandler (single-container only — throws if multi) |
Handlers |
All handlers by name |
GetHandler(name) |
Returns FakeCosmosHandler for a named container |
SetFaultInjector(injector) |
Sets/clears fault injector on ALL handlers |
Database(dbName) |
Returns InMemoryDatabaseResult
|
Databases |
All databases by name |
| Member | Description |
|---|---|
DatabaseName |
The database name |
Containers |
SDK containers in this database by name |
SetupContainer(name) |
Returns IContainerTestSetup for a container in this database |
ExportState(name) |
Export container state as JSON |
ImportState(name, json) |
Import container state from JSON |
ImportStateFromFile(name, filePath) |
Import container state from a file |
ExportStateToFile(name, path) |
Export container state to a file |
RestoreToPointInTime(name, timestamp) |
Restore container to a point in time |
ClearItems(name) |
Remove all items from the container |
GetHandler(name) |
Returns FakeCosmosHandler for a container in this database |
Handlers |
All handlers in this database by name |
SetFaultInjector(injector) |
Sets/clears fault injector on this database's handlers |
| Method | Description |
|---|---|
AddContainer(name, pkPath, configure?) |
Add a container to this database |
AddContainer(name, pkPaths, configure?) |
Add a container with hierarchical partition key |
public class OrderTests : IAsyncLifetime, IAsyncDisposable
{
private readonly InMemoryCosmosResult _cosmos =
InMemoryCosmos.Create("orders", "/customerId");
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => _cosmos.DisposeAsync().AsTask();
[Fact]
public async Task Can_create_and_read_order()
{
await _cosmos.Container.CreateItemAsync(
new Order { Id = "1", CustomerId = "c1" },
new PartitionKey("c1"));
var response = await _cosmos.Container.ReadItemAsync<Order>(
"1", new PartitionKey("c1"));
response.Resource.Id.Should().Be("1");
}
}using var cosmos = InMemoryCosmos.Create("events",
new[] { "/tenantId", "/category", "/date" });
// Query by prefix
var prefixPk = new PartitionKeyBuilder().Add("tenant-1").Build();
var iterator = cosmos.Container.GetItemQueryIterator<Event>(
"SELECT * FROM c",
requestOptions: new QueryRequestOptions { PartitionKey = prefixPk });using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
configureContainer: c =>
{
c.DefaultTimeToLive = 3600;
c.UniqueKeyPolicy = new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = { "/email" } } } };
c.FeedRangeCount = 4;
});// Via configureContainer callback
using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
configureContainer: c =>
{
c.CreateItemAsync(new { id = "1", customerId = "c1" }, new PartitionKey("c1")).Wait();
});
// Via ImportState after creation
cosmos.ImportState(File.ReadAllText("seed-data.json"));
// Via SDK
await cosmos.Container.UpsertItemAsync(order, new PartitionKey(order.CustomerId));// Single container — 429 on all writes
cosmos.Handler.FaultInjector = req =>
{
if (req.Method == HttpMethod.Post)
return new HttpResponseMessage((HttpStatusCode)429);
return null;
};
cosmos.Handler.FaultInjectorIncludesMetadata = false; // don't fault SDK metadata calls
// Per-container
cosmos.GetHandler("orders").FaultInjector = req => ...;
// Global outage
cosmos.SetFaultInjector(req => new HttpResponseMessage((HttpStatusCode)503));
// Clear all injectors
cosmos.SetFaultInjector(null);foreach (var query in cosmos.Handler.QueryLog)
Console.WriteLine(query);
foreach (var request in cosmos.Handler.RequestLog)
Console.WriteLine(request);using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.WrapHandler(h => new CosmosTrackingMessageHandler(trackingOptions, h))
.Build();cosmos.ExportStateToFile("orders-snapshot.json");
cosmos.ImportStateFromFile("orders-snapshot.json");
cosmos.RestoreToPointInTime(DateTimeOffset.UtcNow.AddHours(-1));using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId") // → "default" database
.AddDatabase("audit-db", db => {
db.AddContainer("audit-events", "/eventId");
})
.Build();
// Flat access works (names are unique)
var orders = cosmos.Containers["orders"];
var events = cosmos.Containers["audit-events"];
// Database-scoped access also works
var eventsToo = cosmos.Database("audit-db").Containers["audit-events"];-
DI integration:
UseInMemoryCosmosDB()andUseInMemoryCosmosContainers()work exactly as before — zero changes needed -
InMemoryContainer: Still available internally for direct unit test usage where you pass aContainerto code under test. Access viaIContainerTestSetup(v4.0+) -
InMemoryCosmosClient: Deprecated — useInMemoryCosmos.Create()orInMemoryCosmos.Builder()instead (v4.0+) -
FakeCosmosHandler: Still the underlying HTTP handler — just accessed viacosmos.Handlerinstead of constructed directly - All query, CRUD, and feature behavior: No changes to emulation fidelity
- SQL engine: Same 125+ built-in functions, same parser, same behavior
- All other features: Change feed, transactions, patch, TTL, ETags, bulk, vectors, full-text search, geospatial — all unchanged
These FakeCosmosHandler methods are no longer part of the public API. They are still used internally by InMemoryCosmos — the functionality is preserved, just behind a cleaner interface.
This is a test-code-only breaking change. Production code is unaffected.
| 2.x pattern | 3.0.0 replacement |
|---|---|
handler.CreateClient() |
InMemoryCosmos.Create("name", "/pk") |
FakeCosmosHandler.CreateClient(handlers) |
InMemoryCosmos.Builder().AddContainer(...).Build() |
FakeCosmosHandler.CreateRouter(handlers) |
InMemoryCosmos.Builder().AddContainer(...).Build() |
handler.WrapClient(innerClient) |
InMemoryCosmos.Create("name", "/pk") |
FakeCosmosHandler.WrapClient(innerClient, handlers) |
InMemoryCosmos.Builder().AddContainer(...).Build() |
handler.FaultInjector = ... |
cosmos.Handler.FaultInjector = ... |
handler.QueryLog |
cosmos.Handler.QueryLog |
container.RegisterUdf(...) |
cosmos.SetupContainer().RegisterUdf(...) (or still directly on InMemoryContainer) |
Before (2.x):
var container = new InMemoryContainer("orders", "/customerId");
var handler = new FakeCosmosHandler(container);
var client = handler.CreateClient();
var cosmosContainer = client.GetContainer("db", "orders");After (3.0.0):
using var cosmos = InMemoryCosmos.Create("orders", "/customerId");
var client = cosmos.Client; // if you need the CosmosClient
var container = cosmos.Container; // the SDK ContainerBefore (2.x):
var ordersContainer = new InMemoryContainer("orders", "/customerId");
var ordersHandler = new FakeCosmosHandler(ordersContainer);
var productsContainer = new InMemoryContainer("products", "/categoryId");
var productsHandler = new FakeCosmosHandler(productsContainer);
var handlers = new Dictionary<string, FakeCosmosHandler>
{
["orders"] = ordersHandler,
["products"] = productsHandler
};
var client = FakeCosmosHandler.CreateClient(handlers);After (3.0.0):
using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.AddContainer("products", "/categoryId")
.Build();
var client = cosmos.Client;
var orders = cosmos.Containers["orders"];
var products = cosmos.Containers["products"];Before (2.x):
var router = FakeCosmosHandler.CreateRouter(handlers);
var innerClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ConnectionMode = ConnectionMode.Gateway,
Serializer = mySerializer,
HttpClientFactory = () => new HttpClient(router)
});
var client = FakeCosmosHandler.WrapClient(innerClient, handlers);After (3.0.0):
using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.ConfigureOptions(opts => opts.Serializer = mySerializer)
.Build();Before (2.x):
var router = FakeCosmosHandler.CreateRouter(handlers);
var innerClient = new CosmosClient(connectionString, new CosmosClientOptions
{
ConnectionMode = ConnectionMode.Gateway,
HttpClientFactory = () => new HttpClient(
new CosmosTrackingMessageHandler(trackingOptions, router))
});
var client = FakeCosmosHandler.WrapClient(innerClient, handlers);After (3.0.0):
using var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.WrapHandler(h => new CosmosTrackingMessageHandler(trackingOptions, h))
.Build();Before (2.x):
handler.FaultInjector = req => new HttpResponseMessage((HttpStatusCode)429);After (3.0.0):
cosmos.Handler.FaultInjector = req => new HttpResponseMessage((HttpStatusCode)429); // single container
cosmos.GetHandler("orders").FaultInjector = ...; // per container
cosmos.SetFaultInjector(req => new HttpResponseMessage((HttpStatusCode)503)); // all containersBefore (2.x):
container.RegisterUdf("myUdf", args => (double)args[0] * 2);
container.ImportState(json);
container.ClearItems();After (3.0.0):
cosmos.SetupContainer().RegisterUdf("myUdf", args => (double)args[0] * 2);
cosmos.ImportState(json);
cosmos.ClearItems();No changes needed. UseInMemoryCosmosDB() and UseInMemoryCosmosContainers() continue to work exactly as before. The internal implementation uses InMemoryCosmos under the hood, but the DI API surface is unchanged.
// This code works identically in 2.x and 3.0.0
builder.ConfigureTestServices(services =>
{
services.UseInMemoryCosmosDB(options => options
.AddContainer("orders", "/customerId")
.WithHttpMessageHandlerWrapper(h =>
new CosmosTrackingMessageHandler(trackingOptions, h)));
});Getting Started
Integration & Dependency Injection
Data Management
Reference
Help