Skip to content

Setup Guide

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

Step-by-step guide for wiring CosmosDB.InMemoryEmulator into your test projects — Dependency Injection patterns, custom factory interfaces, and direct instantiation. For installation, see Getting Started. For help choosing the right approach, see Choosing Your Approach. For a cookbook of test examples (CRUD, queries, patches, test isolation), see Unit Testing.


Setup Patterns

The emulator provides three Dependency Injection extension methods on IServiceCollection, plus patterns for custom factory interfaces and direct instantiation:

Pattern Method / Approach Purpose
1 UseInMemoryCosmosDB() Client + Container — the most common setup
2 UseInMemoryCosmosDB<TClient>() Typed CosmosClient subclass
3 UseInMemoryCosmosDB() Client only — repos call GetContainer() directly
4 UseInMemoryCosmosContainers() Container only — no CosmosClient replacement
5 InMemoryCosmos.Create(...) / InMemoryCosmos.Builder() Custom factory interface

All patterns require zero production code changes. Your production Startup.cs / Program.cs stays exactly as-is — including .ToFeedIterator(), LINQ queries, and all SDK operations. The emulator intercepts requests in-process via FakeCosmosHandler.

Patterns 1–4 are one-liner Dependency Injection calls from ConfigureTestServices. Pattern 5 is for projects where Cosmos access is behind a custom factory interface — common in enterprise codebases with internal frameworks.


Pattern 1: Singleton Client + Singleton Container

The most common pattern. Production code registers one CosmosClient and one or more Container instances.

Production Code (unchanged)

// In Startup.cs / Program.cs
services.AddSingleton<CosmosClient>(sp =>
    new CosmosClient(configuration["CosmosDb:ConnectionString"]));

services.AddSingleton<Container>(sp =>
{
    var client = sp.GetRequiredService<CosmosClient>();
    return client.GetContainer("MyDatabase", "orders");
});

Test Setup — Zero-Config (Recommended)

public class MyAppFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.UseInMemoryCosmosDB();
        });
    }
}

That's it! UseInMemoryCosmosDB() creates an InMemoryContainer and wires it through FakeCosmosHandler into a real CosmosClient. Your existing Container factory registrations are preserved — they resolve against the new CosmosClient, which routes all requests through the in-memory handler.

What Happens (Zero-Config)

  1. The existing CosmosClient registration is removed
  2. An InMemoryContainer is created and wrapped in FakeCosmosHandler
  3. A real CosmosClient is created with ConnectionMode.Gateway and the handler intercepting all HTTP traffic
  4. The CosmosClient is registered as a singleton
  5. Existing Container factory registrations are kept — they call client.GetContainer() on the real client, which routes through FakeCosmosHandler

Note: The default in-memory container uses /id as the partition key path. This works for most tests since production code passes explicit PartitionKey values. If your tests rely on partition-key-path-based extraction (e.g. PartitionKey.None), use the explicit form below.

Test Setup — Explicit Containers

Use AddContainer() when you need to control the partition key path:

builder.ConfigureTestServices(services =>
{
    services.UseInMemoryCosmosDB(options => options
        .AddContainer("orders", "/customerId"));
});

What Happens (Explicit)

  1. The existing CosmosClient registration is removed
  2. The existing Container registration(s) are removed
  3. InMemoryContainer instances are created for each AddContainer() call and wrapped in FakeCosmosHandler
  4. A real CosmosClient is registered (singleton), backed by the handler
  5. Container instances are registered (via client.GetContainer()), preserving the original lifetime

Multiple Containers

services.UseInMemoryCosmosDB(options => options
    .AddContainer("orders", "/customerId")
    .AddContainer("customers", "/id")
    .AddContainer("products", "/categoryId"));

Each container is registered as a separate Container service descriptor. If your production code resolves multiple Container instances, they'll each get a distinct in-memory container.

Important — explicit mode is all-or-nothing. As soon as you call AddContainer(), all existing Container registrations are removed and only the containers you explicitly add are registered. If your production code registers three containers but you only call AddContainer() for one of them, the other two will not be available via Dependency Injection. Declare all containers your code depends on.

Seeding Data & Test Setup

Use the Dependency Injection callbacks to seed data, register UDFs/stored procedures, or configure fault injection:

services.UseInMemoryCosmosDB(options =>
{
    options.AddContainer("orders", "/customerId");

    // Called after each FakeCosmosHandler is created
    options.OnHandlerCreated = (containerName, handler) =>
    {
        // Seed data through the backing container
        var backing = handler.BackingContainer;
        backing.CreateItemAsync(
            new { id = "seed-1", customerId = "c1", status = "active" },
            new PartitionKey("c1")).Wait();

        // Configure fault injection
        handler.FaultInjector = req =>
        {
            if (req.Method == HttpMethod.Post)
                return new HttpResponseMessage((HttpStatusCode)429);
            return null;
        };
    };

    // Called after the CosmosClient is created
    options.OnClientCreated = client =>
    {
        // Access the real SDK CosmosClient for advanced scenarios
    };
});

See Seeding Data for more seeding strategies.

Custom Handler Wrapping

Wrap the HTTP handler with a DelegatingHandler for logging, tracking, or metrics:

services.UseInMemoryCosmosDB(options =>
{
    options.AddContainer("orders", "/customerId");
    options.WithHttpMessageHandlerWrapper(handler => new MyTrackingHandler(handler));
});

When to Use

  • Integration/Component/Acceptance tests with WebApplicationFactory
  • Any system that uses Dependency Injection for CosmosClient or Container registrations
  • Code that calls CosmosClient.GetContainer() directly
  • Default choice for most projects

Pattern 2: Typed CosmosClient Subclasses

Used when production code has multiple typed CosmosClient subclasses — each connecting to a different Cosmos DB account or database. Common in multi-tenant or domain-segmented architectures.

Production Code (unchanged)

// Production typed clients
public class EmployeeCosmosClient : CosmosClient
{
    public EmployeeCosmosClient(string connectionString) : base(connectionString) { }
}

public class CustomerCosmosClient : CosmosClient
{
    public CustomerCosmosClient(string connectionString) : base(connectionString) { }
}

// Registration
services.AddSingleton(new EmployeeCosmosClient(config["Biometric:ConnectionString"]));
services.AddSingleton(new CustomerCosmosClient(config["OOB:ConnectionString"]));

// Repos resolve the typed client
public class BiometricRepository
{
    private readonly EmployeeCosmosClient _client;
    public BiometricRepository(EmployeeCosmosClient client) => _client = client;
}

Test Setup

No shadow types needed. Pass your production typed client directly:

builder.ConfigureTestServices(services =>
{
    services.UseInMemoryCosmosDB<EmployeeCosmosClient>(options => options
        .AddContainer("employee-data", "/employeeId"));

    services.UseInMemoryCosmosDB<CustomerCosmosClient>(options => options
        .AddContainer("customer-stuff", "/customerId"));
});

What Happens

  1. The existing EmployeeCosmosClient registration is removed
  2. A Castle.Core dynamic proxy of EmployeeCosmosClient is created, backed by FakeCosmosHandler
  3. The proxy intercepts GetContainer() calls to provide transparent hierarchical partition key prefix support
  4. No Container or base CosmosClient is registered — Pattern 2 repos call client.GetContainer() directly
  5. Repos that depend on EmployeeCosmosClient resolve the proxied version

How It Works

UseInMemoryCosmosDB<TClient>() uses Castle.Core (already a transitive dependency via NSubstitute) to create a runtime subclass of your production TClient. The proxy:

  • Passes a fake connection string and CosmosClientOptions (with FakeCosmosHandler) to the base CosmosClient constructor
  • Intercepts GetContainer() to wrap the returned Container in PartitionKeyCapturingContainer — this is required for correct partition key handling with hierarchical prefix queries
  • Uses GetContainerOnlyHook to restrict proxying to just GetContainer(), avoiding TypeLoadException from CosmosClient's internal virtual methods
  • All other methods pass through to the real CosmosClient base class

Requirements

  • TClient must extend CosmosClient (directly or indirectly)
  • TClient must not be sealed
  • TClient must have a public constructor accepting (string connectionString, CosmosClientOptions options)

Limitations

Limitation Impact
client.GetType() != typeof(TClient) The resolved instance is a Castle proxy subclass. client is TClient is true, but GetType() returns the proxy type. Avoid exact type checks in test assertions
Constructor side effects run with fake connection string If your TClient constructor does more than call base(...), those side effects run with "AccountEndpoint=https://localhost:8081/;AccountKey=..."
Debugger shows proxy type name In the debugger, the instance appears as EmployeeCosmosClientProxy rather than EmployeeCosmosClient
NativeAOT incompatible Castle.Core uses Reflection.Emit — irrelevant for test code but worth noting

Full SDK fidelity. Unlike previous versions (which used InMemoryCosmosClient), Pattern 2 now goes through the real SDK HTTP pipeline via FakeCosmosHandler. This means fault injection, query logging, LINQ .ToFeedIterator(), and serialization testing all work identically to Pattern 1.


Pattern 3: Singleton Client, Repos Call GetContainer()

Production code registers only CosmosClient, and each repository calls client.GetContainer() directly.

Production Code (unchanged)

services.AddSingleton<CosmosClient>(sp =>
    new CosmosClient(configuration["CosmosDb:ConnectionString"]));

public class OrderRepository
{
    private readonly Container _container;

    public OrderRepository(CosmosClient client)
    {
        _container = client.GetContainer("MyDb", "orders");
    }
}

Test Setup

builder.ConfigureTestServices(services =>
{
    services.UseInMemoryCosmosDB(options => options
        .AddContainer("orders", "/customerId")
        .AddContainer("customers", "/id"));
});

When your repositories call client.GetContainer(), the real CosmosClient returns a standard Container proxy. All HTTP requests from that proxy are intercepted by FakeCosmosHandler, which routes them to the pre-configured InMemoryContainer with the correct partition key path. You must declare every container your repositories use via AddContainer().

Note: The SDK's LINQ-to-SQL translation is exercised, giving you higher fidelity than bypassing the SDK entirely. See Choosing Your Approach for a comparison of all approaches.


Pattern 4: Container-Only Replacement

Use when you only want to replace Container registrations but keep the original CosmosClient (or don't have one registered).

New in 4.0: UseInMemoryCosmosContainers() now uses FakeCosmosHandler internally — same full SDK fidelity as UseInMemoryCosmosDB().

Production Code (unchanged)

services.AddSingleton<Container>(sp =>
{
    var client = sp.GetRequiredService<CosmosClient>();
    return client.GetContainer("MyDatabase", "orders");
});

public class OrderRepository(Container container)
{
    public async Task<Order> GetOrder(string id, string partitionKey)
        => (await container.ReadItemAsync<Order>(id, new PartitionKey(partitionKey))).Resource;
}

Test Setup

services.UseInMemoryCosmosContainers(options => options
    .AddContainer("orders", "/customerId"));

What Happens

  1. InMemoryContainer instances are created and wrapped in FakeCosmosHandler
  2. A hidden internal CosmosClient is created with ConnectionMode.Gateway and the handler intercepting all HTTP traffic
  3. Container instances are registered in Dependency Injection (via client.GetContainer()), preserving the original lifetime
  4. CosmosClient is NOT touched — any existing registration remains as-is

Advanced Options

services.UseInMemoryCosmosContainers(options =>
{
    options.AddContainer("orders", "/customerId");
    options.DatabaseName = "TestDb";

    // Access the backing handler for fault injection or logging
    options.OnHandlerCreated = (name, handler) =>
    {
        handler.FaultInjector = req => /* simulate 429s, 503s, etc. */;
    };

    // Access the backing container for seeding and test setup
    options.OnContainerCreated = setup =>
    {
        // Seed data, register UDFs/triggers, etc.
    };

    // Wrap the HTTP handler (e.g. for logging)
    options.WithHttpMessageHandlerWrapper(h => new MyLoggingHandler(h));
});

When to Use

  • Repositories depend on Container directly (injected via Dependency Injection), not CosmosClient
  • The CosmosClient is either not registered or is used only as a factory during startup
  • You want to keep the production CosmosClient registration for other purposes

Limitations

UseInMemoryCosmosContainers() does NOT replace the production CosmosClient. This means:

  • If production code uses CosmosClient.GetContainer(), those containers still point to the production endpoint — only containers resolved directly via IServiceProvider.GetRequiredService<Container>() are replaced. Use UseInMemoryCosmosDB() instead for that scenario.
  • The hidden internal CosmosClient is not accessible via Dependency Injection — use UseInMemoryCosmosDB() with OnClientCreated if you need a CosmosClient reference

Pattern 5: Custom Factory Interface — InMemoryCosmos

Used when production code resolves containers through a custom factory interface (e.g. IDatabaseClientFactory) rather than registering CosmosClient or Container directly in Dependency Injection. Common in enterprise codebases with internal frameworks. Uses InMemoryCosmos.Builder() (or InMemoryCosmos.Create() for single-container).

Test Setup

public class CosmosDbFixture : IDatabaseClientFactory, IDisposable
{
    private readonly InMemoryCosmosResult _cosmos;

    public CosmosDbFixture()
    {
        _cosmos = InMemoryCosmos.Builder()
            .AddContainer("transactions", "/merchantId")
            .AddContainer("idempotency-locks", "/endpoint")
            .AddContainer("orders", "/customerId")
            .AddContainer("audit-log", new[] { "/tenantId", "/category", "/date" })
            .Build();
    }

    public Container GetContainer(string containerName)
        => _cosmos.Containers[containerName];

    /// <summary>Direct access to test setup for registering sprocs, UDFs, triggers.</summary>
    public IContainerTestSetup SetupContainer(string containerName)
        => _cosmos.SetupContainer(containerName);

    /// <summary>Import state for seeding test data.</summary>
    public void ImportStateFromFile(string containerName, string filePath)
        => _cosmos.ImportStateFromFile(filePath, containerName);

    public void Dispose() => _cosmos.Dispose();
}

⚠️ Serialization: camelCase id required. When using FakeCosmosHandler, the Cosmos SDK serializes your C# objects to JSON before sending them over HTTP. If your models use PascalCase Id, the JSON will contain "Id" instead of "id", and upserts/creates will fail with "Item must have an 'id' property". Fix this by configuring the serializer via ConfigureOptions():

_cosmos = InMemoryCosmos.Builder()
    .AddContainer("transactions", "/merchantId")
    .ConfigureOptions(opts => opts.Serializer = new CosmosJsonDotNetSerializer(
        new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        }))
    .Build();

For Patterns 1–4 (the Dependency Injection extension methods), the CosmosClient is created internally with the SDK's default serializer — ensure your models use lowercase id via attributes (e.g. [JsonProperty("id")]) or naming conventions. See Troubleshooting for more common issues.

Register in test Dependency Injection:

builder.ConfigureTestServices(services =>
{
    var fixture = new CosmosDbFixture();
    services.AddSingleton<IDatabaseClientFactory>(fixture);
});

What Happens

  1. InMemoryCosmos.Builder() creates InMemoryContainer instances, wraps each in a FakeCosmosHandler, and builds a real CosmosClient with ConnectionMode.Gateway — all internally
  2. The SDK's full pipeline executes (LINQ → SQL translation, partition key routing, continuation tokens)
  3. .ToFeedIterator() works unchanged — the SDK translates LINQ to SQL, sends it over HTTP, and the handler executes it via the SQL query engine
  4. SQL queries, CRUD, patches, batches — everything goes through the real SDK pipeline

Seeding Test Data

Seed data through the Container returned by the fixture, or use SetupContainer() for direct access:

var container = fixture.GetContainer("transactions");
await container.CreateItemAsync(
    new Transaction { Id = "tx-1", MerchantId = "m-1" },
    new PartitionKey("m-1"));

See Seeding Data for more seeding strategies.

When to Use

  • Production code resolves containers through a custom factory interface (e.g. IDatabaseClientFactory)
  • Cosmos access is abstracted behind an internal framework rather than using CosmosClient or Container directly in Dependency Injection

Limitations

  • Containers must be pre-declared via AddContainer() (not auto-created on demand)
  • Requires a fixture class that implements your factory interface — slightly more setup than Patterns 1–4

Related: For test sequence diagram tracking with Pattern 5, see the TestTrackingDiagrams CosmosDB Extension integration guide. It shows how to wrap the handler with a tracking handler via WrapHandler() to capture Cosmos operations in your test diagrams.


Direct Instantiation

No Dependency Injection required. The simplest way to get full SDK fidelity in unit/component tests. Handles all FakeCosmosHandler wiring internally. For detailed usage examples (CRUD, SQL queries, LINQ, patches, test isolation), see Unit Testing.

Single Container

using var cosmos = InMemoryCosmos.Create("orders", "/customerId");

// Real SDK Container — all calls go through FakeCosmosHandler
var container = cosmos.Container;
await container.CreateItemAsync(order, new PartitionKey(order.CustomerId));

// Test setup
cosmos.SetupContainer().RegisterUdf("discount", args => (double)args[0] * 0.9);

// Fault injection
cosmos.Handler.FaultInjector = req => new HttpResponseMessage((HttpStatusCode)429);

Multi-Container

using var cosmos = InMemoryCosmos.Builder()
    .AddContainer("orders", "/customerId")
    .AddContainer("products", "/categoryId")
    .WrapHandler(h => new MyTrackingHandler(h))       // Only needed if you want to wrap the HTTP handler
    .ConfigureOptions(opts => opts.Serializer = mySerializer) // Only needed if you use a custom serializer
    .Build();

var client = cosmos.Client;
cosmos.SetupContainer("orders").RegisterStoredProcedure("myProc", (pk, args) => "result");
cosmos.GetHandler("orders").FaultInjector = req => ...;
cosmos.SetFaultInjector(req => ...); // all containers

Multi-Database

See Multi-Database for AddDatabase() with overlapping container names.

Hierarchical Partition Keys

using var cosmos = InMemoryCosmos.Create("orders",
    new[] { "/tenantId", "/customerId" });

Configuration Options

InMemoryCosmosOptions (for UseInMemoryCosmosDB and UseInMemoryCosmosDB<TClient>)

services.UseInMemoryCosmosDB(options =>
{
    // Add containers with custom partition key paths
    options.AddContainer("orders", "/customerId");
    options.AddContainer("customers", "/id", databaseName: "customer-db");

    // Override the default database name (defaults to "in-memory-db")
    options.DatabaseName = "my-database";

    // Callback after client creation (e.g. capture reference)
    options.OnClientCreated = client =>
    {
        // client is the original CosmosClient backed by FakeCosmosHandler
    };

    // Callback for each FakeCosmosHandler after creation (e.g. seed data, configure fault injection)
    options.OnHandlerCreated = (containerName, handler) =>
    {
        var backingContainer = handler.BackingContainer;
        backingContainer.CreateItemAsync(new { id = "seed-1", customerId = "c1" },
            new PartitionKey("c1")).Wait();
    };

    // Wrap the FakeCosmosHandler with a custom DelegatingHandler (e.g. for logging, tracking).
    // The function receives the handler/router and must return the outermost handler.
    // Added in v2.0.5.
    options.WithHttpMessageHandlerWrapper(fakeHandler =>
        new MyLoggingHandler(fakeHandler));
});

InMemoryContainerOptions (for UseInMemoryCosmosContainers)

services.UseInMemoryCosmosContainers(options =>
{
    options.AddContainer("orders", "/customerId");

    // Callback for each container after creation
    options.OnContainerCreated = container =>
    {
        container.DefaultTimeToLive = 3600; // 1 hour TTL
    };
});

ContainerConfig Record

public record ContainerConfig(
    string ContainerName,
    string PartitionKeyPath = "/id",
    string? DatabaseName = null);

See the API Reference for full property details on all classes.


Service Lifetime Behaviour

All three Dependency Injection methods preserve the lifetime of existing registrations:

  • If production registered Container as scoped, the in-memory replacement is also scoped
  • If production registered CosmosClient as singleton, the in-memory replacement is also singleton
  • If no existing registration is found, singleton is used as the default

This means your test Dependency Injection graph has the same lifetime characteristics as production.


Complete WebApplicationFactory Example

public class OrderApiTests : IClassFixture<OrderApiFactory>
{
    private readonly HttpClient _client;

    public OrderApiTests(OrderApiFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetOrder_ReturnsOrder()
    {
        // Arrange — seed via API or direct container access
        var response = await _client.PostAsJsonAsync("/api/orders",
            new { customerId = "cust-1", product = "Widget" });
        response.EnsureSuccessStatusCode();

        // Act
        var order = await _client.GetFromJsonAsync<Order>("/api/orders/cust-1");

        // Assert
        order.Should().NotBeNull();
        order!.Product.Should().Be("Widget");
    }
}

public class OrderApiFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.UseInMemoryCosmosDB(options => options
                .AddContainer("orders", "/customerId"));
        });
    }
}

For simpler unit tests that don't need Dependency Injection or WebApplicationFactory, see Unit Testing.


Troubleshooting

"ToFeedIterator is only supported on Cosmos LINQ query operations"

Your production code uses .ToFeedIterator() which requires the real Cosmos SDK LINQ provider. All recommended Dependency Injection patterns (UseInMemoryCosmosDB(), UseInMemoryCosmosContainers()) use FakeCosmosHandler internally, so .ToFeedIterator() works unchanged.

If you see this error, you're likely using an internal class directly. Migrate to one of the recommended patterns — see How Does ToFeedIterator() Work? for details.

Container not found / empty results

Ensure the container names and partition key paths in your test setup match what production code expects:

// If production does: client.GetContainer("MyDb", "orders")
// Then test setup needs:
options.AddContainer("orders", "/customerId");  // container name must match

Multiple Container instances resolve to the same container

Each AddContainer() call creates a separate InMemoryContainer instance. If your production code resolves Container from Dependency Injection, it gets the first one registered. If you need multiple containers, consider Pattern 3 (repos call client.GetContainer()) instead of resolving multiple Container instances from Dependency Injection.

For more troubleshooting tips, see the Troubleshooting page.


See Also

Clone this wiki locally