Skip to content

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

ChangeWho approvesAction
Patch SDK within feature band (10.0.10010.0.108)Platform owner or Renovate/Dependabot PRUpdate global.json sdk.version when adopting a new patch; rollForward: latestPatch may pick patches automatically
Feature band bump (10.0.10010.0.200)Platform owner ADR or standards releaseUpdate standards.manifest.json stack.dotnet, template global.json, and release notes
Major .NET versionStandards major releaseUpdate 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:

RuleBehaviour
IDE0011Braces required for all if, else, foreach, while, and switch bodies, including single-line bodies.
IDE0161File-scoped namespaces required. Block-scoped namespace Foo { } is a build error. This applies to EF Core generated migration files — convert them after generation.
IDE0040Explicit access modifier required on every type member. Interface method implementations must declare public.
// IDE0011 — required braces
if (x > 0)
{
DoSomething();
}
// IDE0161 — file-scoped namespace
namespace MyApp.Domain;
public sealed class Post { }
// IDE0040 — explicit access modifier
public 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
ProjectReferences
{ProjectName}.DomainNothing
{ProjectName}.Application.Write.ContractsDomain
{ProjectName}.Application.Read.ContractsDomain
{ProjectName}.Application.WriteApplication.Write.Contracts, Domain
{ProjectName}.Application.ReadApplication.Read.Contracts, Domain, Microsoft.EntityFrameworkCore
{ProjectName}.Application.ReactionsApplication.Write.Contracts, Application.Read.Contracts, Domain
{ProjectName}.InfrastructureDomain, Application.Write.Contracts, Application.Read.Contracts, Application.Reactions
{ProjectName}.WebApiApplication.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 PackageProject(s)Purpose
LiteBus.Commands.AbstractionsApplication.Write.Contracts, Application.Write, Application.ReactionsICommand, ICommand<TResult>, ICommandHandler<TCommand>, ICommandHandler<TCommand, TResult>, ICommandValidator<TCommand>, ICommandMediator
LiteBus.Queries.AbstractionsApplication.Read.Contracts, Application.ReadIQuery<TResult>, IQueryHandler<TQuery, TResult>, IQueryValidator<TQuery>, IQueryMediator
LiteBus.Events.AbstractionsApplication.Reactions, InfrastructureIEventHandler<TEvent>, IEventPublisher. Do not add this to the Domain project. Domain event classes are plain records with no LiteBus dependency.
LiteBus.Commands.AbstractionsWebApiICommandMediator for command endpoints
LiteBus.Queries.AbstractionsWebApiIQueryMediator for query endpoints
LiteBus.Extensions.Microsoft.DependencyInjectionWebApiFull DI registration for all handlers. Provides AddLiteBus, AddCommandModule, AddQueryModule, AddEventModule.

Namespaces. Each module’s DI extension methods require a separate using:

  • using LiteBus.Commands; for AddCommandModule
  • using LiteBus.Queries; for AddQueryModule
  • using LiteBus.Events; for AddEventModule

Check 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 a public static class {Layer}AssemblyMarker { } so Program.cs in WebApi can pass typeof({Layer}AssemblyMarker).Assembly to RegisterFromAssembly without 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:

PackageLayer(s)Purpose
LiteBus.Commands.AbstractionsApplication.Write.Contracts, Application.Write, Application.Reactions, WebApiCommand mediator abstractions
LiteBus.Queries.AbstractionsApplication.Read.Contracts, Application.ReadQuery mediator abstractions
LiteBus.Events.AbstractionsApplication.ReactionsEvent mediator abstractions
LiteBus.Commands.AbstractionsWebApiICommandMediator for command endpoints
LiteBus.Queries.AbstractionsWebApiIQueryMediator for query endpoints
LiteBus.Extensions.Microsoft.DependencyInjectionWebApiLiteBus DI registration
Ardalis.GuardClausesDomainProject-owned custom guard extensions that throw approved domain exceptions
Microsoft.EntityFrameworkCoreInfrastructure, Application.ReadORM and async LINQ extensions for query handlers
Microsoft.EntityFrameworkCore.SqliteApplication.TestsFast relational query handler tests
Npgsql.EntityFrameworkCore.PostgreSQLInfrastructurePostgreSQL EF Core provider
Serilog.AspNetCoreWebApiStructured logging
Serilog.Sinks.ConsoleWebApiJSON console logs
Serilog.Enrichers.EnvironmentWebApiEnvironment log enrichment
Serilog.Enrichers.ThreadWebApiThread log enrichment
OpenTelemetry.Extensions.HostingWebApi, worker projectsOpenTelemetry registration
OpenTelemetry.Instrumentation.AspNetCoreWebApiHTTP server traces and metrics
OpenTelemetry.Instrumentation.HttpInfrastructure, WebApiOutbound HTTP traces
OpenTelemetry.Instrumentation.RuntimeWebApi, worker projectsRuntime metrics
OpenTelemetry.Exporter.OpenTelemetryProtocolWebApi, worker projectsOTLP export
xunitAll test projectsTest framework
NSubstituteApplication.TestsMocking framework
AwesomeAssertionsAll test projectsAssertion library
Testcontainers.PostgreSqlIntegration.TestsPostgreSQL container for tests
Microsoft.AspNetCore.Mvc.TestingIntegration.TestsWebApplicationFactory<T>
Microsoft.AspNetCore.Authentication.JwtBearerWebApiJWT bearer authentication
Microsoft.AspNetCore.OpenApiWebApiOpenAPI document generation
Microsoft.Extensions.ApiDescription.ServerWebApiBuild-time OpenAPI spec output
Microsoft.Extensions.Options.DataAnnotationsWebApi, InfrastructureValidateDataAnnotations() for options classes
coverlet.collectorAll test projectsCode 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):

PackagePurpose
next, react, react-domFramework (versions pinned in AGENTS.md)
typescriptType checking
openapi-typescriptGenerate api.d.ts from OpenAPI
@tanstack/react-queryServer state cache and mutations
zustandClient UI state
zodValidation (v4 APIs)
react-hook-form, @hookform/resolversForms
tailwindcss, @tailwindcss/postcss, clsx, tailwind-merge, tw-animate-cssStyling and cn helper
@base-ui/reactshadcn/ui CLI v4 (base-nova) primitives
class-variance-authority, lucide-reactshadcn 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/domUnit and component tests
@playwright/testE2E tests
sonnerToasts
date-fnsDates when Temporal is unavailable
@microsoft/signalrRealtime per docs/decisions/signalr-for-real-time-updates.md
eslint, eslint-plugin-boundariesLint and feature boundary enforcement
joseJWT 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 namespaces

Global usings MUST NOT contain:

  • Namespaces for types used in only one or two files (add the using locally 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.