Skip to content

Project Structure

Two-Context Layout

The Library sample follows this structure:

Library/
├── Library.slnx
├── src/
│   ├── Library.Contracts/              # Shared IDs + integration events
│   │   ├── Ids.cs
│   │   └── LendingEvents.cs
│   ├── Library.Catalog/                # Context 1
│   │   ├── Runtime.cs                  # [Runtime], [DataStore], [DispatchModule], [RestApi]
│   │   ├── Domain/
│   │   │   ├── Book.cs                 # [StoredEntity]
│   │   │   ├── Commands/AddBook.cs     # [CommandHandler]
│   │   │   └── Queries/SearchBooks.cs  # [QueryHandler]
│   │   └── Handlers/                   # Integration event handlers
│   └── Library.Lending/                # Context 2 (same shape)
│       ├── Runtime.cs
│       ├── Domain/
│       └── ...
└── test/
    └── Library.Tests/                  # Tests for both contexts
        ├── Catalog/
        │   ├── TestCatalogRuntime.cs
        │   └── Domain/Commands/...     # Mirrors source FS structure
        └── Lending/
            ├── TestLendingRuntime.cs
            └── Domain/Commands/...

The Contracts Assembly

Contracts holds two things:

  1. All TypedIdsBookId, PatronId, LoanId, etc.
  2. Integration event topic classesLendingEvents, CatalogEvents, etc.

Nothing else. No interfaces, no utilities, no shared logic. If you're tempted to put something in Contracts that isn't an ID or an event, it probably belongs in one of the contexts.

Both contexts reference Contracts. Neither context references the other. This is enforced at compile time — if Library.Lending has a <ProjectReference> to Library.Catalog, you've broken the boundary.

Scaling to More Contexts

Four contexts:

src/
├── Library.Contracts/         # All IDs + all event topics
├── Library.Catalog/           # Books, authors, search
├── Library.Lending/           # Patrons, loans, holds
├── Library.Recommendations/   # Reading suggestions
└── Library.Notifications/     # Emails, push notifications

Same pattern. Each context has its own [Runtime], [DataStore], [DispatchModule]. Each references Contracts. None reference each other.

Where Infrastructure Goes

Infrastructure packages (Postgres, Service Bus, etc.) are referenced in the composition root — the project with Program.cs. Not in the domain projects.

Library.AppHost/Library.AppHost.csproj
<ItemGroup>
    <PackageReference Include="Deepstaging.Data.Postgres" />
    <PackageReference Include="Deepstaging.Cloud.Azure" />
</ItemGroup>

The domain projects only reference Deepstaging (the core package). They don't know or care whether they're backed by Postgres or in-memory stores.

One Test Project or Many?

Either works. The Library sample uses one test project with folders per context. If your contexts are large, you might prefer one test project per context — same as the eShop sample:

test/
├── Library.Catalog.Tests/
├── Library.Lending.Tests/
└── Library.Integration.Tests/    # Cross-context flows

Test file structure mirrors source structure within each context folder.