Repository navigation
Beta
The current FakeCosmosHandler has two patterns for creating a CosmosClient:
Option A — handler.CreateClient() (simple but sealed):
var container = new InMemoryContainer("orders", "/customerId");
var handler = new FakeCosmosHandler(container);
var client = handler.CreateClient();
var cosmosContainer = client.GetContainer("db", "orders");Three objects. Zero extension points. The entry point is FakeCosmosHandler — an HTTP
message handler — which is a confusing, non-intuitive starting point for someone who just
wants an in-memory Cosmos container to test against.
Option B — Manual wiring (extensible but verbose):
var container = new InMemoryContainer("orders", "/customerId");
var handler = new FakeCosmosHandler(container);
var innerClient = new CosmosClient(
"AccountEndpoint=https://localhost:9999/;AccountKey=dGVzdGtleQ==;",
new CosmosClientOptions
{
ConnectionMode = ConnectionMode.Gateway,
LimitToEndpoint = true,
HttpClientFactory = () => new HttpClient(
new CosmosTrackingMessageHandler(trackingOptions, handler))
});
var client = handler.WrapClient(innerClient);
var cosmosContainer = client.GetContainer("db", "orders");~15 lines. Adding TestTrackingDiagrams forces a complete rewrite from A to B.
public static class InMemoryCosmos
{
// Sugar for single-container (delegates to builder internally)
public static InMemoryCosmosResult Create(
string containerName,
string partitionKeyPath = "/id",
Func<HttpMessageHandler, HttpMessageHandler>? wrapHandler = null,
Action<CosmosClientOptions>? configureOptions = null,
Action<InMemoryContainer>? configureContainer = null);
public static InMemoryCosmosResult Create(
string containerName,
string[] partitionKeyPaths,
Func<HttpMessageHandler, HttpMessageHandler>? wrapHandler = null,
Action<CosmosClientOptions>? configureOptions = null,
Action<InMemoryContainer>? configureContainer = null);
// Builder for multi-container or when you want fluent config
public static InMemoryCosmosBuilder Builder();
}public sealed class InMemoryCosmosBuilder
{
public InMemoryCosmosBuilder AddContainer(string name, string partitionKeyPath,
Action<InMemoryContainer>? configure = null);
public InMemoryCosmosBuilder AddContainer(string name, string[] partitionKeyPaths,
Action<InMemoryContainer>? configure = null);
public InMemoryCosmosBuilder WrapHandler(
Func<HttpMessageHandler, HttpMessageHandler> wrapper);
public InMemoryCosmosBuilder ConfigureOptions(
Action<CosmosClientOptions> configure);
public InMemoryCosmosResult Build();
}Three tiers of access:
public sealed class InMemoryCosmosResult : IDisposable
{
// Tier 1 — Production-like (what code-under-test uses)
public CosmosClient Client { get; }
public Container Container { get; } // single-container; throws if multi
public IReadOnlyDictionary<string, Container> Containers { get; }
// Tier 2 — Test setup (sprocs, UDFs, triggers, state, reset)
public IContainerTestSetup SetupContainer(string? containerName = null);
// Tier 3 — Fault injection & diagnostics
public FakeCosmosHandler Handler { get; } // single-container; throws if multi
public IReadOnlyDictionary<string, FakeCosmosHandler> Handlers { get; }
public FakeCosmosHandler GetHandler(string name);
public void SetFaultInjector(Func<HttpRequestMessage, HttpResponseMessage?>? injector); // sets on ALL handlers
}public interface IContainerTestSetup
{
// C# delegate registrations (always available)
void RegisterStoredProcedure(string id, StoredProcedureHandler 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 CosmosDB.InMemoryEmulator.JsTriggers package)
// Throws NotImplementedException with helpful message if JsTriggers not installed
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();
}InMemoryContainer implements IContainerTestSetup. JS overloads check for registered
IJsTriggerEngine/ISprocEngine/IJsUdfEngine; if absent, throw:
NotImplementedException: "JavaScript stored procedure execution requires the
CosmosDB.InMemoryEmulator.JsTriggers package. Install it and call
container.UseJsTriggers(), or use the C# delegate overload instead."
var cosmos = InMemoryCosmos.Create("orders");
await cosmos.Container.CreateItemAsync(
new Order { Id = "1", Name = "Widget" }, new PartitionKey("1"));
var response = await cosmos.Container.ReadItemAsync<Order>("1", new PartitionKey("1"));var cosmos = InMemoryCosmos.Create("orders", "/customerId");
await cosmos.Container.CreateItemAsync(
new Order { Id = "1", CustomerId = "c1" }, new PartitionKey("c1"));
var response = await cosmos.Container.ReadItemAsync<Order>("1", new PartitionKey("c1"));var cosmos = InMemoryCosmos.Create("orders", "/customerId",
wrapHandler: h => new CosmosTrackingMessageHandler(trackingOptions, h));
await cosmos.Container.CreateItemAsync(...);var cosmos = InMemoryCosmos.Create("orders", "/customerId",
wrapHandler: h => new CosmosTrackingMessageHandler(trackingOptions, h),
configureOptions: o => o.Serializer = new MyCamelCaseSerializer());
await cosmos.Container.CreateItemAsync(...);var cosmos = InMemoryCosmos.Create("events", new[] { "/tenantId", "/category", "/date" });
var prefixPk = new PartitionKeyBuilder().Add("tenant-1").Build();
var iterator = cosmos.Container.GetItemQueryIterator<Event>(
"SELECT * FROM c",
requestOptions: new QueryRequestOptions { PartitionKey = prefixPk });var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.AddContainer("customers", "/id")
.Build();
var ordersContainer = cosmos.Containers["orders"];
var customersContainer = cosmos.Containers["customers"];
await ordersContainer.CreateItemAsync(
new Order { Id = "1", CustomerId = "c1" }, new PartitionKey("c1"));
await customersContainer.CreateItemAsync(
new Customer { Id = "c1", Name = "Alice" }, new PartitionKey("c1"));var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId")
.AddContainer("customers", "/id")
.AddContainer("events", new[] { "/tenantId", "/category", "/date" })
.WrapHandler(h => new CosmosTrackingMessageHandler(trackingOptions, h))
.ConfigureOptions(o => o.Serializer = new MyCamelCaseSerializer())
.Build();
var ordersContainer = cosmos.Containers["orders"];
var eventsContainer = cosmos.Containers["events"];// Single-container
var cosmos = InMemoryCosmos.Create("orders", "/customerId");
cosmos.Handler.FaultInjector = req =>
{
if (req.Method == HttpMethod.Post)
return new HttpResponseMessage((HttpStatusCode)429);
return null;
};
cosmos.Handler.FaultInjectorIncludesMetadata = false;
// Multi-container — per-container
cosmos.GetHandler("orders").FaultInjector = req => ...;
// Multi-container — global outage across all containers
cosmos.SetFaultInjector(req => new HttpResponseMessage((HttpStatusCode)503));foreach (var query in cosmos.Handler.QueryLog)
Console.WriteLine(query);cosmos.SetupContainer().RegisterStoredProcedure("mysproc", (args, context) =>
{
context.Response = new { result = "ok" };
return Task.CompletedTask;
});
cosmos.SetupContainer().RegisterUdf("toUpper", args => ((string)args[0]!).ToUpper());
cosmos.SetupContainer().RegisterTrigger("audit", TriggerType.Post, TriggerOperation.All,
doc => { doc["audited"] = true; return doc; });cosmos.SetupContainer().RegisterStoredProcedure("mysproc",
"function(prefix) { var context = getContext(); ... }");
cosmos.SetupContainer().RegisterUdf("toUpper",
"function(s) { return s.toUpperCase(); }");
cosmos.SetupContainer().RegisterTrigger("validate", TriggerType.Pre, TriggerOperation.Create,
"function() { var context = getContext(); ... }");// Export
var json = cosmos.ExportState();
cosmos.ExportStateToFile("seed.json");
// Import
cosmos.ImportState(json);
cosmos.ImportStateFromFile("seed.json");
// Point-in-time restore
cosmos.RestoreToPointInTime(DateTimeOffset.UtcNow.AddHours(-1));cosmos.ClearItems();
// Multi-container:
cosmos.ClearItems("orders");
cosmos.ClearItems("customers");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");
}
}-
InMemoryCosmos— static entry point -
InMemoryCosmosBuilder— fluent builder -
InMemoryCosmosResult— result with Container/Setup/Handler access -
IContainerTestSetup— interface for test-only operations (implemented by InMemoryContainer) -
InMemoryDatabaseBuilder— Phase 3: database-scoped builder callback -
InMemoryDatabaseResult— Phase 3: database-scoped result with Containers/Handlers
| Property | What it's for | When you need it |
|---|---|---|
cosmos.Container |
CRUD, queries, LINQ | Every test |
cosmos.SetupContainer() |
Sprocs, UDFs, triggers | Test setup |
cosmos.ExportState() / ImportState() / ClearItems()
|
State management | Test setup/teardown |
cosmos.Handler |
Fault injection, query log | Chaos testing, diagnostics |
-
Builder + Create sugar:
Create()for single-container (80% case);Builder()for multi-container or advanced config -
IContainerTestSetupinterface: Filters InMemoryContainer surface to test-only methods; avoids exposing 100+ Container methods via IntelliSense - JS overloads with helpful errors: JS body string overloads on IContainerTestSetup throw NotImplementedException with package install guidance when JsTriggers not installed
-
No
BackingContainerexposure: Users access test setup throughIContainerTestSetup; handler throughFakeCosmosHandler -
Direct Container property:
cosmos.Containereliminates need forcosmos.Client.GetContainer("db", "orders")with meaningless "db" string -
wrapHandler param: Same
Func<HttpMessageHandler, HttpMessageHandler>shape as existing DIWithHttpMessageHandlerWrapper()— consistent across DI and non-DI paths -
CosmosClientOptions guardrails: The user's
configureOptionscallback runs first, then:-
ConnectionModeis forcibly set toConnectionMode.Gateway(required by FakeCosmosHandler) -
LimitToEndpointis forcibly set totrue - If the user set
HttpClientFactory, throwInvalidOperationException: "Do not set HttpClientFactory directly — use the wrapHandler parameter to customize the HTTP pipeline. HttpClientFactory is managed internally by InMemoryCosmos." - This lets users safely configure serializers, timeouts, etc. without accidentally breaking the fake handler pipeline
-
-
Builder validation:
-
Build()with no containers (across bothAddContainer()andAddDatabase()) throwsInvalidOperationException: "At least one container must be added before calling Build()." -
AddContainer()with a duplicate name throwsInvalidOperationException: "Container 'orders' has already been added."
-
-
Multiple WrapHandler/ConfigureOptions calls compose: Both chain in order.
.WrapHandler(a).WrapHandler(b)producesb(a(handler))(middleware convention)..ConfigureOptions(a).ConfigureOptions(b)runs both callbacks sequentially. -
Container name casing:
Containersdictionary usesStringComparer.Ordinal— case-sensitive, matching real Cosmos DB behavior. -
Dispose:
InMemoryCosmosResultimplements bothIDisposableandIAsyncDisposable(matchingCosmosClient). Supports bothusing var cosmos = ...andawait using var cosmos = .... Disposes theCosmosClient. Does not clear backing container data (consistent with real Cosmos — disposing client doesn't delete data). -
Invalid name lookups:
SetupContainer("nonexistent")andGetHandler("nonexistent")throwInvalidOperationExceptionlisting available container names: "Container 'nonexistent' not found. Available containers: orders, customers."Containers["nonexistent"]gives standardKeyNotFoundException. -
Input validation:
- Null/empty container name throws
ArgumentException - Empty partition key paths array throws
ArgumentException - Partition key path not starting with
/throwsArgumentException: "Partition key path must start with '/'. Got: 'customerId'."
- Null/empty container name throws
-
Single-container populates Containers:
InMemoryCosmos.Create("orders")populates bothcosmos.Containerandcosmos.Containers["orders"]— they return the same instance. -
Handlers are per-container: Each container gets its own
FakeCosmosHandlerwith independentFaultInjector,QueryLog, etc.Handlersdictionary mirrorsContainers.SetFaultInjector()convenience method sets the injector on all handlers at once for simulating global outages. -
Builder reuse:
Build()can be called multiple times on the same builder. Each call creates a fresh, independentInMemoryCosmosResultwith new containers and handlers. The builder acts as a reusable template. Mutations to the builder betweenBuild()calls are reflected in subsequent results (mutable builder, immutable results). -
Container configuration:
AddContainersupports an optional config callback for TTL, unique keys, computed properties, and feed range count:The.AddContainer("orders", "/customerId", container => { container.DefaultTimeToLive = 3600; container.UniqueKeyPolicy = new UniqueKeyPolicy { ... }; container.ComputedProperties = new[] { ... }; container.FeedRangeCount = 4; })
Create()sugar also gets a config callback overload:InMemoryCosmos.Create("orders", "/customerId", configureContainer: c => c.DefaultTimeToLive = 3600);
-
SetFaultInjector overwrites per-handler injectors: Calling
cosmos.SetFaultInjector(...)overwrites any previously set per-handler injectors on every handler.SetFaultInjector(null)clears them all back to no injection. Type isFunc<HttpRequestMessage, HttpResponseMessage?>?— same as the existingFakeCosmosHandler.FaultInjectorproperty. No named delegate type introduced. -
FakeCosmosHandlerOptions (CacheTtl, CacheMaxEntries, PartitionKeyRangeCount): Not
exposed on builder — these are niche tuning knobs. Configure post-build via the handler:
cosmos.Handler.CacheTtl = TimeSpan.FromMinutes(5); -
Database name:
InMemoryCosmos.DefaultDatabaseNameis aconst stringwith value"default". Used for the rare case where users needcosmos.Client.GetContainer(db, name). Primary path iscosmos.Container/cosmos.Containers["name"]which avoids the question entirely. The router ignores the database name — any value works. Phase 3 introduces explicit multi-database support;DefaultDatabaseNameis the database used byAddContainer()andCreate(). -
Breaking change (3.0.0): Remove
CreateClient(),WrapClient(), andCreateRouter()fromFakeCosmosHandler.InMemoryCosmosreplaces all three patterns. This is a test-code-only breaking change — production code is unaffected. No known external users. -
DI coexistence: Document that
InMemoryCosmosis for non-DI usage. For DI-based integration tests, continue usingUseInMemoryCosmosDB()/UseInMemoryCosmosContainers(). -
Auto-persist with Dispose: When
StatePersistenceDirectoryis configured viaconfigureContainer, the container auto-saves on dispose.InMemoryCosmosResult.Dispose()disposes the client, which triggers container cleanup — auto-persist fires as expected. -
ProductionExtensions not needed: Since
InMemoryCosmosalways usesFakeCosmosHandler,.ToFeedIterator()works natively via the SDK pipeline. TheProductionExtensionspackage is unnecessary — document this to avoid confusion. -
configureContainer callback timing: The
configureContainer/configurecallback runs after theInMemoryContaineris fully constructed and wired to itsFakeCosmosHandler. Safe to callImportState(),RegisterStoredProcedure(), etc. inside the callback. -
Multiple
Create()calls are independent: EachInMemoryCosmos.Create()call produces a fully independent result — separateInMemoryContainer,FakeCosmosHandler, andCosmosClient. No shared state between results. -
Phase 3
Containersambiguity detection (implementation note): The flatContainersdictionary requires a customIReadOnlyDictionary<string, Container>wrapper to throw ambiguity messages on indexer access — standardDictionarythrows genericKeyNotFoundExceptionwithout custom guidance. -
Builder
AddContainerrequires PK path (no default): UnlikeCreate()which defaults to/id, the builder'sAddContainer(name, partitionKeyPath)has no default. Multi-container setups are more deliberate — omitting the PK path is more likely a mistake than an intentional/idchoice.
-
src/CosmosDB.InMemoryEmulator/InMemoryCosmos.cs— new file: static class + builder + result -
src/CosmosDB.InMemoryEmulator/IContainerTestSetup.cs— new file: interface -
src/CosmosDB.InMemoryEmulator/InMemoryContainer.cs— implement IContainerTestSetup, add JS overloads with NotImplementedException -
src/CosmosDB.InMemoryEmulator/FakeCosmosHandler.cs— existing CreateClient/WrapClient/CreateRouter used internally by builder - Both wikis need updating after implementation
After the core InMemoryCosmos API ships, add support for runtime container creation and
deletion via the SDK — enabling production startup code that calls CreateContainerAsync
/ DeleteContainerAsync to work without pre-registration.
-
CreateContainerAsync(containerProperties): Router intercepts the request, creates a newInMemoryContainer+FakeCosmosHandlerfrom theContainerProperties(PK path, unique keys, TTL, etc.), adds them to theContainersandHandlersdictionaries, applies the capturedwrapHandlerfrom build time, and returns 201. -
DeleteContainerAsync: Removes the container and handler from the dictionaries. Subsequent operations on that container return 404. -
ContainersandHandlersare backed byConcurrentDictionarybehind theIReadOnlyDictionaryinterface — read-only access, but contents may grow/shrink. -
SetupContainer()andGetHandler()work for dynamically created containers. - Dynamically created containers get default
FakeCosmosHandlerOptions. -
Container(singular property) behavior: if created viaCreate()(single-container), the shortcut still points to the original container — dynamic additions don't change it.
-
POST /dbs/{db}/colls— CreateContainerAsync -
DELETE /dbs/{db}/colls/{coll}— DeleteContainerAsync -
GET /dbs/{db}/colls/{coll}— ReadContainerAsync (already partially supported) - Thread-safe via
ConcurrentDictionary
-
PUT /dbs/{db}/colls/{coll}—ReplaceContainerAsync: Replaces mutable container configuration (TTL, unique keys, indexing policy, etc.) from the updatedContainerProperties. Existing items are preserved. Partition key path changes are rejected with 400 BadRequest — matching real Cosmos DB, where PK path is immutable after creation. Returns 200. -
POST /dbs/{db}/collswithIfNotExists—CreateContainerIfNotExistsAsync: If the container already exists, returns 200 with existing properties. Otherwise creates and returns 201. This is the most common variant in production startup code. -
POST /dbs—CreateDatabaseAsync: No-op success (201). The database name is noted internally soReadDatabaseAsyncreturns it, but no state is created until containers are added. -
POST /dbswithIfNotExists—CreateDatabaseIfNotExistsAsync: Returns 200 if exists, 201 if created. Same no-op semantics asCreateDatabaseAsync. -
GET /dbs/{db}—ReadDatabaseAsync: Returns 200 if the database was created or has containers; 404 otherwise. -
DELETE /dbs/{db}—DeleteDatabaseAsync: Removes the database and all its containers and handlers. Subsequent operations return 404. -
GET /dbs—ReadAllDatabases/ iterating databases: Returns all known database names. -
GET /dbs/{db}/colls—GetContainerQueryIterator/ listing containers: Returns all container names within the specified database. -
Dynamic deletion + singular Container: If
Create("orders")then production code callsDeleteContainerAsync("orders"), thecosmos.Containerproperty still references the original SDKContainerobject, but the router will 404 all subsequent requests through it. This matches real Cosmos behavior (holding aContainerreference after deletion doesn't throw — operations on it do).
For codebases where a single CosmosClient talks to multiple databases that may have
overlapping container names (e.g. users-db/events and orders-db/events).
Phase 1 and Phase 2 code must remain identical — zero additional ceremony for the 99% single-database case.
public sealed class InMemoryCosmosBuilder
{
// Existing — adds to DefaultDatabaseName
public InMemoryCosmosBuilder AddContainer(string name, string partitionKeyPath,
Action<InMemoryContainer>? configure = null);
// New — explicit database scoping
public InMemoryCosmosBuilder AddDatabase(string databaseName,
Action<InMemoryDatabaseBuilder> configure);
}
public sealed class InMemoryDatabaseBuilder
{
public InMemoryDatabaseBuilder AddContainer(string name, string partitionKeyPath,
Action<InMemoryContainer>? configure = null);
public InMemoryDatabaseBuilder AddContainer(string name, string[] partitionKeyPaths,
Action<InMemoryContainer>? configure = null);
}public sealed class InMemoryCosmosResult : IDisposable, IAsyncDisposable
{
// Existing — work when container names are globally unique
public Container Container { get; }
public IReadOnlyDictionary<string, Container> Containers { get; }
public IContainerTestSetup SetupContainer(string? containerName = null);
// State management convenience methods
public string ExportState(string? containerName = null);
public void ExportStateToFile(string path, string? containerName = null);
public void ImportState(string json, string? containerName = null);
public void ImportStateFromFile(string filePath, string? containerName = null);
public void RestoreToPointInTime(DateTimeOffset timestamp, string? containerName = null);
public void ClearItems(string? containerName = null);
public FakeCosmosHandler Handler { get; }
public IReadOnlyDictionary<string, FakeCosmosHandler> Handlers { get; }
// New — database-scoped access
public InMemoryDatabaseResult Database(string databaseName);
public IReadOnlyDictionary<string, InMemoryDatabaseResult> Databases { get; }
}
public sealed class InMemoryDatabaseResult
{
public string DatabaseName { get; }
public IReadOnlyDictionary<string, Container> Containers { get; }
public IContainerTestSetup SetupContainer(string containerName);
// State management convenience methods
public string ExportState(string containerName);
public void ExportStateToFile(string containerName, string path);
public void ImportState(string containerName, string json);
public void ImportStateFromFile(string containerName, string filePath);
public void RestoreToPointInTime(string containerName, DateTimeOffset timestamp);
public void ClearItems(string containerName);
public FakeCosmosHandler GetHandler(string containerName);
public IReadOnlyDictionary<string, FakeCosmosHandler> Handlers { get; }
public void SetFaultInjector(Func<HttpRequestMessage, HttpResponseMessage?>? injector);
}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");
})
.WrapHandler(h => new CosmosTrackingMessageHandler(trackingOptions, h))
.Build();
// Database-scoped access
var userEvents = cosmos.Database("users-db").Containers["events"];
var orderEvents = cosmos.Database("orders-db").Containers["events"];
// Setup & fault injection scoped to a database's container
cosmos.Database("users-db").SetupContainer("events").RegisterUdf(...);
cosmos.Database("orders-db").GetHandler("events").FaultInjector = ...;
// Global outage — one database
cosmos.Database("orders-db").SetFaultInjector(req =>
new HttpResponseMessage((HttpStatusCode)503));
// Global outage — all databases, all containers
cosmos.SetFaultInjector(req => new HttpResponseMessage((HttpStatusCode)503));var cosmos = InMemoryCosmos.Builder()
.AddContainer("orders", "/customerId") // goes to DefaultDatabaseName
.AddDatabase("audit-db", db => {
db.AddContainer("events", "/eventId");
})
.Build();
// Flat access works — names are globally unique
var orders = cosmos.Containers["orders"];
var events = cosmos.Containers["events"];
// Database-scoped also works
var eventsToo = cosmos.Database("audit-db").Containers["events"];The flat convenience properties (Containers, SetupContainer(), Handlers,
GetHandler()) work when container names are globally unique across all databases.
When names collide, they throw with guidance:
-
cosmos.Containers["events"]→KeyNotFoundException: "Container 'events' exists in multiple databases: users-db, orders-db. Use cosmos.Database("users-db").Containers["events"] instead." -
cosmos.SetupContainer("events")→InvalidOperationException: "Container 'events' exists in multiple databases: users-db, orders-db. Use cosmos.Database("users-db").SetupContainer("events") instead." -
cosmos.GetHandler("events")→ same pattern. -
cosmos.Container(singular): throws if multi-container, regardless of databases. Unaffected by Phase 3. -
cosmos.Handler(singular): same — single-container only.
The HTTP router already receives database name in the URL path
(/dbs/{db}/colls/{coll}/...). Phase 1/2 ignore it. Phase 3 matches on both
{db} and {coll} segments to route to the correct handler. Requests for
unregistered database/container combinations return 404.
-
AddDatabase()with a duplicate database name throwsInvalidOperationException: "Database 'orders-db' has already been added." -
AddDatabase()with an empty callback (no containers) throwsInvalidOperationException: "Database 'orders-db' must have at least one container." - Duplicate container names within a database throw (same as Phase 1).
- Duplicate container names across databases are allowed — that's the whole point.
-
Database("nonexistent")throwsInvalidOperationException: "Database 'nonexistent' not found. Available databases: users-db, orders-db."
The Create() static method is unchanged — it always uses DefaultDatabaseName and
produces a single-database, single-container result. Multi-database requires the builder.
Dynamic CreateContainerAsync extracts the database name from the URL path. If the
database was registered via AddDatabase(), the new container joins that database.
If not, a new InMemoryDatabaseResult is created on the fly (same ConcurrentDictionary
thread-safety). Dynamic containers in ambiguous situations follow the same
ambiguity-throws rules.
- Cross-database transactions
After all three phases are implemented and tested, update all documentation:
-
README.md — Update quick-start examples to use
InMemoryCosmos.Create()and builder. Remove references toFakeCosmosHandler.CreateClient()/WrapClient()/CreateRouter(). -
Wiki — Integration Approaches — Rewrite to show unified API as the primary approach.
Document the three tiers (Container, SetupContainer, Handler) and the progression from
Create()toBuilder()toAddDatabase(). -
Wiki — DI Patterns — Add note that
InMemoryCosmosis for non-DI usage; DI continues usingUseInMemoryCosmosDB()/UseInMemoryCosmosContainers(). -
Wiki — FakeCosmosHandler — Document removal of
CreateClient(),WrapClient(),CreateRouter()in 3.0.0. Update fault injection examples to usecosmos.Handler. -
Wiki — Feed Iterator — Note that
ProductionExtensionsare unnecessary when usingInMemoryCosmos(FakeCosmosHandler pipeline handles it). -
Wiki — Seeding Data — Update examples to use
cosmos.ImportState()andconfigureContainercallback. - Wiki — Container Management — Add Phase 2 dynamic container/database CRUD examples.
-
Wiki — Multi-Database — New page for Phase 3:
AddDatabase(),Database(), ambiguity handling, mixingAddContainer()andAddDatabase(). - Wiki — Troubleshooting — Update for new API error messages and common migration issues.
-
Wiki — Migration Guide — New page: 2.x → 3.0.0 migration guide covering the
CreateClient()/WrapClient()/CreateRouter()removal and replacement patterns. - NuGet package description / release notes — Update for 3.0.0 breaking changes.
Getting Started
Integration & Dependency Injection
Data Management
Reference
Help