Type to search pages and rules.

Browse pages

.NET Architecture

A .NET backend holds its business code in two projects, the shared kernel and the modules, with hosts that compose and serve them. This page binds that shape for .NET: which projects exist, how modules aggregates and use cases fill one namespace tree, which folders each level allows, and where host-only endpoints live, so every type keeps one home and boundaries hold by folder and mark. The shared architecture rules live on Backend Architecture; this page holds only their .NET bindings.

Markdown12 provisions

Stack: dotnet

Layer: application

Concepts#

The Shared Kernel, the Modules and Their Hosts#

The business code lives in two projects. {Product}.Modules holds the modules and nothing else: each module with its aggregates, use cases, reactions and jobs, plus a Composition/ folder for what names several modules at once, such as the module registration and the published event catalog. {Product}.SharedKernel holds what every module builds on and names no module: shared domain values, the command pipeline positions, store and outbox plumbing, and what endpoints of two or more modules compile against, such as IEndpoint and the current actor. Modules references the shared kernel, and the shared kernel references no module. Both projects take the product name as their root namespace, so a module type reads {Product}.Organizations.Teams.UseCases.CreateTeam and a shared kernel type reads {Product}.Domain.

The WebApi host holds Program composition and HTTP plumbing, and the Worker host exists only when background work needs its own process. Endpoints live in their use case folders and the host maps them through the one public assembly marker. Problem Details, authentication, OpenAPI and rate limiting stay in the host.

The host also keeps the endpoints that have no handler to sit beside: protocol endpoints such as sign-out and device callbacks, static assets such as scripts it serves, and development-only tooling. Each such endpoint still carries its mark.

The Module Tree and Its Layers#

Every node of the tree may hold the same layer folders. Each level allows a closed set of folder names, so any other folder is refused. Inside a layer folder subfolders stay free, because the folder already states the layer and the lowest-folder rule already placed the type.

An aggregate keeps its use cases in its UseCases/ folder, one folder per use case, and a module keeps the use cases no single aggregate owns in its own UseCases/ folder, beside one On{Fact}/ folder per reaction. A module Contracts/ folder sorts its public surface by kind: Identifiers/, Exceptions/, IntegrationEvents/, Commands/ and Ports/, so a reader sees at a glance what another module may name.

A use case file takes its layer from its role suffix: Endpoint RequestModel and ResponseModel are WebApi, and every other file in a use case, reaction or job folder is Application. A typed identifier that another module names lives in its owner Contracts/Identifiers/ folder, and shared values live in the shared kernel Domain/ folder. The shared kernel WebApi/ folder holds only what endpoints of two or more modules compile against, while product-wide HTTP plumbing stays in the host.

A module with one aggregate of its own name stays flat. Models stay separate and flow one way: request model, command, aggregate, result, response model. The endpoint maps in and out, and the handler returns a result and never the aggregate.

Visibility and Composition#

A type stays visible only to the assemblies that need it: implementations stay internal sealed, and a message port or marker turns public only when another assembly names it. Service registration lives in the hosts, with each module registering its own Infrastructure types in its module Infrastructure folder for the host to call in one visible order.

Standards#

Mirror Modules Aggregates and Use Cases in One Namespace Tree#

ruleruleguidance
litespec/backend/dotnet/architecture#mirror-modules-aggregates-and-use-cases-in-one-namespace-tree
Statement

Every type in the shared kernel and modules projects MUST live in the namespace tree that starts with the product name and follows the folders below the project root.

Applies toartifact: any

Why

The namespace reads like the qualified identifier, so Claimwick.Spending.Expenses.UseCases.SubmitExpense names the product module aggregate and use case without a map. Namespaces follow folders under litespec/workspace/naming#keep-namespaces-aligned-with-folders, so a reader moves from a Domain aggregate to its handler its repository and its endpoint by name alone. This provision binds litespec/backend/architecture#organize-code-by-module-aggregate-and-use-case for .NET.

Example

The Expenses aggregate of the Spending module fills one branch:

Text
Claimwick.Modules/Spending/Expenses/Domain/Expense.cs
Claimwick.Modules/Spending/Expenses/UseCases/SubmitExpense/SubmitExpenseCommandHandler.cs

The handler above lives in namespace Claimwick.Spending.Expenses.UseCases.SubmitExpense.

Checka reviewer answers

Reviewed forWhich types miss the product module aggregate use case namespace or name one outside its folder? Look at the namespaces beside the folders in the shared kernel and modules projects.

Marked guidance: a reviewer answers this question.

Keep Closed Folder Sets at Every Level#

rulerulecheckable
litespec/backend/dotnet/architecture#keep-closed-folder-sets-at-every-level
Statement

Each level of the shared kernel and modules projects MUST hold only the folders the closed set table in this section allows.

Applies toartifact: any

Why

A closed set refuses the catch-all folder by construction: anything outside the table has no home and review refuses it. Product-wide plumbing lives in the product layer folders and cross-module contracts in each module Contracts folder, while litespec/backend/architecture#place-a-type-at-the-lowest-folder-that-holds-its-consumers decides between a use case folder and the aggregate module and product layer folders. Module folders mirror record homes: use cases sit in UseCases/ folders, reactions in On{Fact}/ folders beside the module use cases, and jobs in the module Jobs/ folder.

Example

LevelAllowed folders
Shared kernel project rootDomain/, Application/, Infrastructure/, WebApi/
Modules project rootComposition/, one per declared module
ModuleContracts/, Domain/, Application/, Infrastructure/, WebApi/, UseCases/, Jobs/, one per declared aggregate
Module Contracts/Identifiers/, Exceptions/, IntegrationEvents/, Commands/, Ports/
Module UseCases/one per declared module use case, one On{Fact}/ per reaction
AggregateDomain/, Application/, Infrastructure/, WebApi/, UseCases/
Aggregate UseCases/one per declared use case
Use case, On{Fact}/Files only
Text
{Product}.SharedKernel/
  Domain/  Application/  Infrastructure/  WebApi/
{Product}.Modules/
  Composition/
  Organizations/
    Contracts/
      Identifiers/  Exceptions/  IntegrationEvents/  Commands/  Ports/
    Domain/  Application/  Infrastructure/  WebApi/
    Teams/
      Domain/          Team.cs  TeamState.cs  ITeamRepository.cs  Events/  Exceptions/
      Application/     types used by two or more Team use cases
      Infrastructure/  TeamRepository.cs
      WebApi/          transport types used by two or more Team use cases
      UseCases/
        CreateTeam/    use case, flat: Command, Result, Handler, Validator, Endpoint, RequestModel, ResponseModel
    UseCases/
      ReadTeamDirectory/        a use case no single aggregate owns
      OnTeamMemberRemoved/      a reaction
    Jobs/
Checkchecked by a tool

Checked bylitespec model check reports mark-folder-drift for a folder outside its closed set or naming no declared record.

Marked checkable: the tool named here checks it.

Keep Application Types in Their Layer Folders#

ruleruleguidance
litespec/backend/dotnet/architecture#keep-application-types-in-their-layer-folders
Statement

An application MUST place commands queries results handlers validators and module ports in Application folders and cross-module contracts in the sorted kind folders of its Contracts folder.

Applies toartifact: any

Why

One use case stays flat in its folder, with its command query result handler validator endpoint and transport models side by side. Types used by two or more use cases of one aggregate live in the aggregate Application folder, types used by two or more aggregates in the module Application folder, and product-wide plumbing in the shared kernel Application folder. A module publishes its typed identifiers in Contracts/Identifiers/, its refusals in Contracts/Exceptions/, its integration events in Contracts/IntegrationEvents/ and its cross-module ports in Contracts/Ports/, so the public surface of a module stays visible at the module.

Checka reviewer answers

Reviewed forWhich commands queries results handlers validators or ports sit outside their Application folder or their Contracts kind folder? Look at the Application and Contracts folders beside the use case folders.

Marked guidance: a reviewer answers this question.

Keep Implementation Types Internal#

rulerulecheckable
litespec/backend/dotnet/architecture#keep-implementation-types-internal
Statement

A handler, validator, definition, persistence implementation, or endpoint implementation MUST be declared internal sealed.

Applies toartifact: command-handler, query-handler, validator, reaction, repository, endpoint

Why

A public type is an offer to every assembly, so an implementation left public invites callers it was never written for. Messages, results, markers, and ports stay public only when another assembly uses them; handlers, validators, and implementations stay internal sealed. Assembly scanning reads internal types through the one public marker, so it never justifies widening visibility.

This provision binds litespec/backend/architecture#keep-implementation-types-hidden for .NET.

Checkchecked by a tool

Checked bydotnet build reports LS2001 for an implementation type that is not internal sealed.

Marked checkable: the tool named here checks it.

Follow Layer Direction in Every File#

rulerulecheckable
litespec/backend/dotnet/architecture#follow-layer-direction-in-every-file
Statement

A type MUST reference only the layers its own layer allows under the layer table in this section.

Applies toartifact: any

Why

Layers are roles played by folders and use case files, whatever project holds them. A file takes its layer from its folder and, inside a use case folder, from its role suffix. Global usings import namespaces for every file, so the rule reads them too and they hide coupling from nothing. This provision binds litespec/backend/architecture#point-dependencies-inward for .NET.

Example

LayerHoldsMay reference
DomainAggregates, state records, value objects, domain events, domain exceptions, repository contractsDomain
ApplicationCommands, queries, results, handlers, validators, ports, pipeline positionsDomain, Application
InfrastructureRepositories, adapters, mappings, module registrationDomain, Application, Infrastructure
WebApiEndpoints, request and response models, transport mappingsApplication, WebApi, and from Domain only typed identifiers and shared-kernel values

Domain touches no Application Infrastructure or WebApi type and none of the persistence web mediator logging and injection namespaces. Application touches no Infrastructure or WebApi type and no ASP.NET Core type. WebApi touches no Domain type outside typed identifiers and shared-kernel values and no repository.

Checkchecked by a tool

Checked bydotnet build reports LS1001 for a Domain type naming an outer-layer type and LS2018 for an Application or WebApi type naming a type outside its layer.

Marked checkable: the tool named here checks it.

Keep Use Case Folders Isolated#

rulerulecheckable
litespec/backend/dotnet/architecture#keep-use-case-folders-isolated
Statement

A use case folder MUST reference no type in another use case folder, except the message and result of a use case it runs.

Applies toartifact: use-case, command-handler, query-handler, validator, endpoint, reaction

Why

A use case folder holds one use case flat, so a reference between two such folders couples two slices that change for different reasons. A reaction or a use case that runs another use case sends that use case's command or query and reads its result, which is the published way in, so those types stay nameable. A type named by two use cases belongs in the aggregate module or shared kernel layer folder that holds every consumer naming it, under litespec/backend/architecture#place-a-type-at-the-lowest-folder-that-holds-its-consumers. The rule applies to files and to global usings alike.

Checkchecked by a tool

Checked bydotnet build reports LS2019 for a use case folder naming a type in another use case folder, other than one whose outermost type ends in Command, Query, CommandResult or QueryResult.

Marked checkable: the tool named here checks it.

Reach Another Module Only Through Contracts#

rulerulecheckable
litespec/backend/dotnet/architecture#reach-another-module-only-through-contracts
Statement

A module MUST reach another module only through types its Contracts folder publishes.

Applies toartifact: any

Why

The Contracts folder is the public surface of a module, sorted by kind: typed identifiers in Identifiers/, the refusals of rules other modules enforce in Exceptions/, integration events in IntegrationEvents/, the commands and queries other modules send with their results in Commands/, and ports that other modules call in Ports/. Domain may name the typed identifiers and refusals; ports and integration events stay outside Domain. A module that needs another module data reads it through a read port in the owner Contracts/Ports/ folder that returns contract results keyed by typed identifier, or answers with its own query; it loads no foreign aggregate or repository. A command changes only its own module aggregates, and an act on another module aggregate runs as that module use case, dispatched or triggered by an integration event. A reaction to another module fact subscribes to the published integration event and never to the domain event.

Checkchecked by a tool

Checked bydotnet build reports LS2020 for a module reference naming another module type outside its Contracts folder.

Marked checkable: the tool named here checks it.

Follow Declared Module Dependencies#

ruleruleguidance
litespec/backend/dotnet/architecture#follow-declared-module-dependencies
Statement

A module MUST reference only modules its record names in dependsOn.

Applies toartifact: any

Why

The dependsOn list states which modules a module may know, so an undeclared edge is a boundary the design never allowed. The rule covers code references and record links alike, and the graph stays acyclic.

Checka reviewer answers

Reviewed forWhich module references name a module outside their owner dependsOn, or close a dependency cycle? Look at the cross-module references beside the module records.

Marked guidance: a reviewer answers this question.

Keep Host Endpoints in Hosts#

ruleruleguidance
litespec/backend/dotnet/architecture#keep-host-endpoints-in-hosts
Statement

Each host endpoint without a handler MUST live in its host project.

Applies toartifact: endpoint

Why

Protocol endpoints, static assets, and development-only tooling have no use case folder to sit beside, so the host keeps them. Each still carries its mark, and the placement check accepts a host endpoint in the host.

Checka reviewer answers

Reviewed forWhich host endpoints without a handler sit outside their host project, or lack their mark? Look at the host endpoints beside the use case folders.

Marked guidance: a reviewer answers this question.

Conventions#

Separate the Shared Kernel from the Modules#

conventiondefaultguidance
litespec/backend/dotnet/architecture#separate-the-shared-kernel-from-the-modules
Default

Use a {Product}.SharedKernel project for what every module builds on and a {Product}.Modules project for the modules, with WebApi and optional Worker hosts, at every size.

ReplacementA project records its own project layout in its host documentation.

Applies toartifact: any

Why

The modules project then holds only business code and module slices, so a reader opening it sees the modules of the product and nothing else, and one use case stays in one tree instead of spreading across layer projects. The shared kernel names no module, so a change to one module never touches it, and the build refuses a shared kernel type that reaches into a module because the shared kernel project does not reference the modules project. Hosts stay separate projects, because two hosts can run the same modules.

Example

Text
apps/api/src/{Product}.SharedKernel/
apps/api/src/{Product}.Modules/
apps/api/src/{Product}.WebApi/
apps/api/src/{Product}.Worker/     only when background work needs its own process
Checka reviewer answers

Reviewed forWhich projects sit outside the shared kernel, the modules project and their hosts, and does the shared kernel reference the modules project? Look at the solution file and the project references.

Marked guidance: a reviewer answers this question.

Keep Composition in Hosts#

conventiondefaultguidance
litespec/backend/dotnet/architecture#keep-composition-in-hosts
Default

Keep service registration in Program.cs and host registration modules.

ReplacementA project names its registration layout in its host documentation.

Applies toartifact: any

Why

Domain and Application declare abstractions for the hosts to implement. A registration call inside either layer makes the abstraction depend on the thing it abstracts, inverting the dependency direction. Each module registers its own Infrastructure types in its module Infrastructure folder, and Program.cs calls each module entry point in one visible order.

This provision binds litespec/backend/architecture#keep-service-registration-in-hosts for .NET.

Example

One registration entry point per module plus product plumbing has this shape:

Text
AddOrganizationsInfrastructure(...)
AddTeamsInfrastructure(...)
AddWebApi(...)

Program.cs keeps their call order visible. The WebApi entry point owns Problem Details, authentication, authorization, endpoint discovery, and OpenAPI.

Checka reviewer answers

Reviewed forWhich service registration calls sit outside Program.cs and the host registration modules? Look at the registration calls.

Marked guidance: a reviewer answers this question.

Use One Public Assembly Marker#

conventiondefaultguidance
litespec/backend/dotnet/architecture#use-one-public-assembly-marker
Default

Expose one public static assembly marker in the modules project for host endpoint discovery and scanning.

ReplacementA project names its scanner assembly reference in its host documentation.

Applies toartifact: any

Why

Scanners and the host need an assembly reference, and without a marker developers make handlers public to supply one. One public static marker in the modules project gives the host and the scanners their reference while every handler stays internal.

Checka reviewer answers

Reviewed forDoes the modules project expose one public static assembly marker, and does any other project expose one it needs not? Look at each project for its marker.

Marked guidance: a reviewer answers this question.