Skip to content

Whats New In 3.0

Aryeh Citron edited this page Apr 20, 2026 · 5 revisions

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.


Headline Features

1. InMemoryCosmos — Unified Entry Point

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

2. Dynamic Container Management

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.

3. Multi-Database Support

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 name

When 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.

4. IContainerTestSetup Interface

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");

5. Builder Extensibility

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

Complete API Reference

InMemoryCosmos (Static Class)

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

InMemoryCosmosBuilder

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

InMemoryCosmosResult : IDisposable, IAsyncDisposable

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

InMemoryDatabaseResult

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

InMemoryDatabaseBuilder

Method Description
AddContainer(name, pkPath, configure?) Add a container to this database
AddContainer(name, pkPaths, configure?) Add a container with hierarchical partition key

Usage Patterns

Test Fixture

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");
    }
}

Hierarchical Partition Keys

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 });

Container Configuration (TTL, Unique Keys, etc.)

using var cosmos = InMemoryCosmos.Create("orders", "/customerId",
    configureContainer: c =>
    {
        c.DefaultTimeToLive = 3600;
        c.UniqueKeyPolicy = new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = { "/email" } } } };
        c.FeedRangeCount = 4;
    });

Seeding Data

// 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));

Fault Injection

// 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);

Query Logging

foreach (var query in cosmos.Handler.QueryLog)
    Console.WriteLine(query);

foreach (var request in cosmos.Handler.RequestLog)
    Console.WriteLine(request);

TestTrackingDiagrams Integration

using var cosmos = InMemoryCosmos.Builder()
    .AddContainer("orders", "/customerId")
    .WrapHandler(h => new CosmosTrackingMessageHandler(trackingOptions, h))
    .Build();

State Persistence Between Tests

cosmos.ExportStateToFile("orders-snapshot.json");
cosmos.ImportStateFromFile("orders-snapshot.json");
cosmos.RestoreToPointInTime(DateTimeOffset.UtcNow.AddHours(-1));

Mixing AddContainer and AddDatabase

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"];

What's Unchanged

  • DI integration: UseInMemoryCosmosDB() and UseInMemoryCosmosContainers() work exactly as before — zero changes needed
  • InMemoryContainer: Still available internally for direct unit test usage where you pass a Container to code under test. Access via IContainerTestSetup (v4.0+)
  • InMemoryCosmosClient: Deprecated — use InMemoryCosmos.Create() or InMemoryCosmos.Builder() instead (v4.0+)
  • FakeCosmosHandler: Still the underlying HTTP handler — just accessed via cosmos.Handler instead 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

Migration Guide: 2.x → 3.0.0

Breaking Change: CreateClient(), WrapClient(), CreateRouter() are now internal

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.

Quick Reference

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)

Single Container

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 Container

Multi-Container

Before (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"];

Custom CosmosClientOptions

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();

Handler Wrapping (TestTrackingDiagrams, Logging, etc.)

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();

Fault Injection

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 containers

Test Setup

Before (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();

DI Code

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)));
});

Clone this wiki locally