Solution Structure
This document covers the physical structure of a .NET solution following these standards.
1. Solution Layout
Every project follows this internal layout. Replace {ProjectName} with the actual project name (PascalCase, no spaces).
Monorepo (default): the solution root is apps/api/ in the repository. See docs/conventions/shared/monorepo-structure.md.
Single-project repository: the solution root is the repository root. See docs/guides/single-project-setup.md.
apps/api/ ← solution root (monorepo) or repository root (single-project)├── global.json├── Directory.Build.props├── Directory.Packages.props├── {ProjectName}.slnx├── src/│ ├── {ProjectName}.Domain/│ ├── {ProjectName}.Application.Write.Contracts/│ ├── {ProjectName}.Application.Write/│ ├── {ProjectName}.Application.Read.Contracts/│ ├── {ProjectName}.Application.Read/│ ├── {ProjectName}.Application.Reactions/│ ├── {ProjectName}.Infrastructure/│ ├── {ProjectName}.WebApi/│ ├── {ProjectName}.AppHost/ ← .NET Aspire orchestration│ └── {ProjectName}.ServiceDefaults/ ← shared OTel, health checks├── tests/│ ├── {ProjectName}.Domain.Tests/│ ├── {ProjectName}.Application.Tests/│ ├── {ProjectName}.Integration.Tests/│ └── {ProjectName}.Architecture.Tests/└── docs/ └── decisions/The solution file uses the .slnx format (SDK-style solution files), not the legacy .sln format.
The AppHost project is the local development entry point. Run dotnet run --project apps/api/src/{ProjectName}.AppHost (monorepo) or dotnet run --project src/{ProjectName}.AppHost (single-project) to start all services including the database container. See docs/conventions/backend/deployment-and-migrations.md for Aspire setup details.
Every Aspire solution MUST include a {ProjectName}.ServiceDefaults project. The WebApi MUST call builder.AddServiceDefaults() and app.MapDefaultEndpoints() in Development.
Every solution MUST commit .config/dotnet-tools.json at the repository root with a pinned dotnet-ef version matching Microsoft.EntityFrameworkCore in Directory.Packages.props. Copy docs/templates/config/dotnet-tools.json.
2. global.json
Every solution MUST include a global.json at the solution root that pins the .NET SDK version. Copy docs/templates/config/global.json.
{ "sdk": { "version": "10.0.100", "rollForward": "latestPatch" }}rollForward: latestPatch allows patch-level SDK updates (10.0.101, 10.0.102) without requiring a global.json update, while preventing major or minor version drift. This ensures all contributors and CI agents use the same SDK minor version.
SDK feature-band update policy
| Change | Who approves | Action |
|---|---|---|
Patch SDK within feature band (10.0.100 → 10.0.108) | Platform owner or Renovate/Dependabot PR | Update global.json sdk.version when adopting a new patch; rollForward: latestPatch may pick patches automatically |
Feature band bump (10.0.100 → 10.0.200) | Platform owner ADR or standards release | Update standards.manifest.json stack.dotnet, template global.json, and release notes |
| Major .NET version | Standards major release | Update manifest, templates, and consumer upgrade guide |
Check Microsoft .NET support policy before bumping SDK or runtime versions.
This file MUST be committed to source control. It MUST NOT appear in .gitignore.
3. Directory.Build.props
A Directory.Build.props file at the solution root sets metadata shared across all projects. Copy docs/templates/config/Directory.Build.props or use:
<Project> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> </PropertyGroup></Project>TreatWarningsAsErrors ensures that nullable reference warnings, unused variable warnings, and similar issues are build failures, not silent warnings. Do not set LangVersion to preview in production projects. The SDK default language version for the pinned .NET release is the standard. Use LangVersion=preview only when a project ADR documents an experimental repository.
EnforceCodeStyleInBuild promotes IDE-only style rules to build-time errors. The following rules are enforced and will cause build failures if violated:
| Rule | Behaviour |
|---|---|
| IDE0011 | Braces required for all if, else, foreach, while, and switch bodies, including single-line bodies. |
| IDE0161 | File-scoped namespaces required. Block-scoped namespace Foo { } is a build error. This applies to EF Core generated migration files — convert them after generation. |
| IDE0040 | Explicit access modifier required on every type member. Interface method implementations must declare public. |
// IDE0011 — required bracesif (x > 0){ DoSomething();}
// IDE0161 — file-scoped namespacenamespace MyApp.Domain;
public sealed class Post { }
// IDE0040 — explicit access modifierpublic sealed class PostConfiguration : IEntityTypeConfiguration<Post>{ public void Configure(EntityTypeBuilder<Post> builder) { } // explicit public required}4. Directory.Packages.props
All NuGet package versions are managed centrally via Directory.Packages.props at the solution root. Copy docs/templates/config/Directory.Packages.props and pin versions from standards.manifest.json.
<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> <ItemGroup> <PackageVersion Include="LiteBus.Commands.Abstractions" Version="x.x.x" /> <PackageVersion Include="LiteBus.Queries.Abstractions" Version="x.x.x" /> <PackageVersion Include="LiteBus.Events.Abstractions" Version="x.x.x" /> <PackageVersion Include="LiteBus.Extensions.Microsoft.DependencyInjection" Version="x.x.x" /> <PackageVersion Include="Ardalis.GuardClauses" Version="x.x.x" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="x.x.x" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="x.x.x" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="x.x.x" /> <PackageVersion Include="Serilog.AspNetCore" Version="x.x.x" /> <PackageVersion Include="Serilog.Sinks.Console" Version="x.x.x" /> <PackageVersion Include="Serilog.Enrichers.Environment" Version="x.x.x" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="x.x.x" /> <PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="x.x.x" /> <PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="x.x.x" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="x.x.x" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="x.x.x" /> <PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="x.x.x" /> <PackageVersion Include="xunit" Version="x.x.x" /> <PackageVersion Include="NSubstitute" Version="x.x.x" /> <PackageVersion Include="AwesomeAssertions" Version="x.x.x" /> <PackageVersion Include="Testcontainers.PostgreSql" Version="x.x.x" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="x.x.x" /> <PackageVersion Include="coverlet.collector" Version="x.x.x" /> </ItemGroup></Project>Individual .csproj files reference packages without version attributes:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="LiteBus.Commands.Abstractions" /> <PackageReference Include="Ardalis.GuardClauses" /> </ItemGroup></Project>5. Project References
The dependency rule (outer layers depend on inner layers, never the reverse) is enforced via project references. The reference graph below is the authoritative source.
graph TD Domain["Domain"] WriteContracts["Application.Write.Contracts"] ReadContracts["Application.Read.Contracts"] Write["Application.Write"] Read["Application.Read"] Reactions["Application.Reactions"] Infra["Infrastructure"] WebApi["WebApi"]
WriteContracts --> Domain ReadContracts --> Domain Write --> WriteContracts Write --> Domain Read --> ReadContracts Read --> Domain Reactions --> WriteContracts Reactions --> ReadContracts Reactions --> Domain Infra --> Domain Infra --> WriteContracts Infra --> ReadContracts Infra --> Reactions WebApi --> WriteContracts WebApi --> ReadContracts| Project | References |
|---|---|
{ProjectName}.Domain | Nothing |
{ProjectName}.Application.Write.Contracts | Domain |
{ProjectName}.Application.Read.Contracts | Domain |
{ProjectName}.Application.Write | Application.Write.Contracts, Domain |
{ProjectName}.Application.Read | Application.Read.Contracts, Domain, Microsoft.EntityFrameworkCore |
{ProjectName}.Application.Reactions | Application.Write.Contracts, Application.Read.Contracts, Domain |
{ProjectName}.Infrastructure | Domain, Application.Write.Contracts, Application.Read.Contracts, Application.Reactions |
{ProjectName}.WebApi | Application.Write.Contracts, Application.Read.Contracts |
{ProjectName}.WebApi (Program.cs only) | Infrastructure, Application.Write, Application.Read, Application.Reactions for DI registration |
No circular references exist anywhere in this graph. If a reference would create a cycle, the design is wrong.
6. Which LiteBus Package Goes Where
LiteBus is modular. Each project references only the package it needs. Never add the full LiteBus metapackage to a project that only needs abstractions.
| LiteBus Package | Project(s) | Purpose |
|---|---|---|
LiteBus.Commands.Abstractions | Application.Write.Contracts, Application.Write, Application.Reactions | ICommand, ICommand<TResult>, ICommandHandler<TCommand>, ICommandHandler<TCommand, TResult>, ICommandValidator<TCommand>, ICommandMediator |
LiteBus.Queries.Abstractions | Application.Read.Contracts, Application.Read | IQuery<TResult>, IQueryHandler<TQuery, TResult>, IQueryValidator<TQuery>, IQueryMediator |
LiteBus.Events.Abstractions | Application.Reactions, Infrastructure | IEventHandler<TEvent>, IEventPublisher. Do not add this to the Domain project. Domain event classes are plain records with no LiteBus dependency. |
LiteBus.Commands.Abstractions | WebApi | ICommandMediator for command endpoints |
LiteBus.Queries.Abstractions | WebApi | IQueryMediator for query endpoints |
LiteBus.Extensions.Microsoft.DependencyInjection | WebApi | Full DI registration for all handlers. Provides AddLiteBus, AddCommandModule, AddQueryModule, AddEventModule. |
Namespaces. Each module’s DI extension methods require a separate
using:
using LiteBus.Commands;forAddCommandModuleusing LiteBus.Queries;forAddQueryModuleusing LiteBus.Events;forAddEventModuleCheck the current LiteBus docs for the precise package and namespace names. The LiteBus package structure may change between major versions.
Assembly markers. Handler classes are
internal sealed. Each implementation project must expose apublic static class {Layer}AssemblyMarker { }soProgram.csinWebApican passtypeof({Layer}AssemblyMarker).AssemblytoRegisterFromAssemblywithout depending on internal types.
7. NuGet Package Policy
Every new NuGet package MUST be justified with an ADR in docs/decisions/. The ADR explains why the package was chosen, what alternatives were considered, and what the trade-offs are.
The following packages are pre-approved and do not require a new ADR:
| Package | Layer(s) | Purpose |
|---|---|---|
LiteBus.Commands.Abstractions | Application.Write.Contracts, Application.Write, Application.Reactions, WebApi | Command mediator abstractions |
LiteBus.Queries.Abstractions | Application.Read.Contracts, Application.Read | Query mediator abstractions |
LiteBus.Events.Abstractions | Application.Reactions | Event mediator abstractions |
LiteBus.Commands.Abstractions | WebApi | ICommandMediator for command endpoints |
LiteBus.Queries.Abstractions | WebApi | IQueryMediator for query endpoints |
LiteBus.Extensions.Microsoft.DependencyInjection | WebApi | LiteBus DI registration |
Ardalis.GuardClauses | Domain | Project-owned custom guard extensions that throw approved domain exceptions |
Microsoft.EntityFrameworkCore | Infrastructure, Application.Read | ORM and async LINQ extensions for query handlers |
Microsoft.EntityFrameworkCore.Sqlite | Application.Tests | Fast relational query handler tests |
Npgsql.EntityFrameworkCore.PostgreSQL | Infrastructure | PostgreSQL EF Core provider |
Serilog.AspNetCore | WebApi | Structured logging |
Serilog.Sinks.Console | WebApi | JSON console logs |
Serilog.Enrichers.Environment | WebApi | Environment log enrichment |
Serilog.Enrichers.Thread | WebApi | Thread log enrichment |
OpenTelemetry.Extensions.Hosting | WebApi, worker projects | OpenTelemetry registration |
OpenTelemetry.Instrumentation.AspNetCore | WebApi | HTTP server traces and metrics |
OpenTelemetry.Instrumentation.Http | Infrastructure, WebApi | Outbound HTTP traces |
OpenTelemetry.Instrumentation.Runtime | WebApi, worker projects | Runtime metrics |
OpenTelemetry.Exporter.OpenTelemetryProtocol | WebApi, worker projects | OTLP export |
xunit | All test projects | Test framework |
NSubstitute | Application.Tests | Mocking framework |
AwesomeAssertions | All test projects | Assertion library |
Testcontainers.PostgreSql | Integration.Tests | PostgreSQL container for tests |
Microsoft.AspNetCore.Mvc.Testing | Integration.Tests | WebApplicationFactory<T> |
Microsoft.AspNetCore.Authentication.JwtBearer | WebApi | JWT bearer authentication |
Microsoft.AspNetCore.OpenApi | WebApi | OpenAPI document generation |
Microsoft.Extensions.ApiDescription.Server | WebApi | Build-time OpenAPI spec output |
Microsoft.Extensions.Options.DataAnnotations | WebApi, Infrastructure | ValidateDataAnnotations() for options classes |
coverlet.collector | All test projects | Code coverage |
Any package not in this list requires an ADR before being added to any project.
Project File Templates
The following .csproj templates show the minimal, correct project references and package references for each layer. Adjust for your project name.
apps/api/src/{ProjectName}.Domain/{ProjectName}.Domain.csproj:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Ardalis.GuardClauses" /> </ItemGroup></Project>apps/api/src/{ProjectName}.Application.Write.Contracts/{ProjectName}.Application.Write.Contracts.csproj:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\{ProjectName}.Domain\{ProjectName}.Domain.csproj" /> <PackageReference Include="LiteBus.Commands.Abstractions" /> </ItemGroup></Project>apps/api/src/{ProjectName}.Infrastructure/{ProjectName}.Infrastructure.csproj:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\{ProjectName}.Domain\{ProjectName}.Domain.csproj" /> <ProjectReference Include="..\{ProjectName}.Application.Write.Contracts\{ProjectName}.Application.Write.Contracts.csproj" /> <ProjectReference Include="..\{ProjectName}.Application.Read.Contracts\{ProjectName}.Application.Read.Contracts.csproj" /> <ProjectReference Include="..\{ProjectName}.Application.Reactions\{ProjectName}.Application.Reactions.csproj" /> <PackageReference Include="Microsoft.EntityFrameworkCore" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" /> <PackageReference Include="LiteBus.Commands.Abstractions" /> <PackageReference Include="LiteBus.Events.Abstractions" /> </ItemGroup></Project>apps/api/tests/{ProjectName}.Architecture.Tests/{ProjectName}.Architecture.Tests.csproj:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\..\src\{ProjectName}.Application.Write\{ProjectName}.Application.Write.csproj" /> <ProjectReference Include="..\..\src\{ProjectName}.Application.Read\{ProjectName}.Application.Read.csproj" /> <ProjectReference Include="..\..\src\{ProjectName}.Application.Reactions\{ProjectName}.Application.Reactions.csproj" /> <PackageReference Include="xunit" /> <PackageReference Include="AwesomeAssertions" /> <PackageReference Include="NetArchTest.Rules" /> </ItemGroup></Project>NetArchTest.Rules is a pre-approved package. Add it to Directory.Packages.props.
8. npm Package Policy (Frontend Monorepo)
Every new npm package MUST be justified with an ADR unless listed below. Forbidden packages are in docs/conventions/shared/forbidden-packages.md.
Pre-approved npm packages (no new ADR required):
| Package | Purpose |
|---|---|
next, react, react-dom | Framework (versions pinned in AGENTS.md) |
typescript | Type checking |
openapi-typescript | Generate api.d.ts from OpenAPI |
@tanstack/react-query | Server state cache and mutations |
zustand | Client UI state |
zod | Validation (v4 APIs) |
react-hook-form, @hookform/resolvers | Forms |
tailwindcss, @tailwindcss/postcss, clsx, tailwind-merge, tw-animate-css | Styling and cn helper |
@base-ui/react | shadcn/ui CLI v4 (base-nova) primitives |
class-variance-authority, lucide-react | shadcn component variants and icons |
next-auth / auth (Auth.js v5) | Authentication per docs/decisions/authjs-v5-authentication.md |
vitest (4.x), @testing-library/react, @testing-library/dom | Unit and component tests |
@playwright/test | E2E tests |
sonner | Toasts |
date-fns | Dates when Temporal is unavailable |
@microsoft/signalr | Realtime per docs/decisions/signalr-for-real-time-updates.md |
eslint, eslint-plugin-boundaries | Lint and feature boundary enforcement |
jose | JWT signing for server-to-server API authentication (Admin apps only, per docs/conventions/frontend/admin-api-auth.md) |
Owned source copied into the repo (for example vendored openapi-fetch in packages/api-client/) is allowed when documented in docs/decisions/openapi-typescript-client-generation.md.
9. GlobalUsings.cs
Each project contains a single GlobalUsings.cs file at the project root. Global usings reduce repetition but MUST only contain namespaces used in the majority of files in that project.
{ProjectName}.Domain/└── GlobalUsings.cs <- only domain-wide namespaces
{ProjectName}.Application.Write/└── GlobalUsings.cs <- only application-write-wide namespacesGlobal usings MUST NOT contain:
- Namespaces for types used in only one or two files (add the
usinglocally instead) - Aliases that could be confused with standard library types
- Infrastructure namespaces in Domain or Application global usings
Project-specific configuration is documented in the project repository. See docs/templates/ in the standards repository for the templates to use.