Skip to content

Service Project Structure Guide

“Should this code go in Domain or Application?” “What rules should the folder structure and naming follow when adding a new Adapter?” “In which layer should the Port interface be placed?”

As a project grows, decisions about code placement become increasingly difficult. Without clear project structure rules, layer dependencies become entangled and you must discuss where to add new features every time. This guide provides a consistent answer to the question “WHERE to place code.”

Through this document, you will learn:

  1. 8-project structure and dependency direction - Roles and reference rules for Domain, Application, 3 Adapters, Host, and 2 Test projects
  2. 3-step code placement decision - Layer decision → project/folder decision → Port placement judgment
  3. Primary and secondary objective concepts - Distinguishing core code from supporting infrastructure in each project
  4. Host’s Composition Root role - Layer registration order and middleware pipeline configuration
  5. Test project configuration - Folder structure and settings for unit tests and integration tests

A basic understanding of the following concepts is required to understand this document:

  • Basic concepts of Hexagonal Architecture (Ports and Adapters)
  • .NET project references (ProjectReference) and NuGet package references
  • Basic principles of DI (Dependency Injection) containers

The core of project structure is establishing consistent rules for code placement, maintaining dependency direction from outside to inside between layers.

Terminal window
# Build
dotnet build {ServiceName}.slnx
# Test
dotnet test --solution {ServiceName}.slnx
# Architecture test (dependency direction verification)
# Automatically verified by LayerDependencyArchitectureRuleTests

1. Code placement decision (3 steps):

  1. Layer decision — Business rules (Domain), use case orchestration (Application), technical implementation (Adapter)
  2. Project and folder decision — Refer to the project/folder mapping table by code type
  3. Port placement judgment — Domain if method signatures use only domain types, Application if external DTOs are included

2. New service project creation:

  1. Domain project (AssemblyReference.cs, Using.cs, AggregateRoots/)
  2. Application project (Usecases/, Ports/)
  3. 3 Adapters (Presentation, Persistence, Infrastructure)
  4. Host project (Program.cs — layer registration)
  5. Tests.Unit + Tests.Integration
ConceptDescription
8-project structureDomain, Application, 3 Adapters, Host, 2 Tests
Dependency directionOutside → inside (Host → Adapter → Application → Domain)
Primary / secondary objectivesPrimary is business/technical code, secondary is supporting infrastructure like DI registration
Abstractions/ folderSecondary objectives of Adapter projects (Registrations/, Options/, Extensions/)
Port locationAggregate-specific CRUD → Domain, external systems → Application

This guide covers the project structure of a service — folder names, file placement, and dependency direction. “HOW to implement” is delegated to other guides, and we focus only on “WHERE to place.”

WHERE (this guide)HOW (reference guides)
AggregateRoots folder structure06a-aggregate-design.md (design) + 06b-entity-aggregate-core.md (core patterns) + 06c-entity-aggregate-advanced.md (advanced patterns)
ValueObjects location rules05a-value-objects.md — Value Object implementation patterns
Specifications location rules10-specifications.md — Specification pattern implementation
Domain Ports location criteria12-ports.md — Port architecture and design principles
Usecases folder/file naming11-usecases-and-cqrs.md — Use case implementation
Abstractions/Registrations structure14a-adapter-pipeline-di.md — DI registration code patterns
WHY (module mapping rationale)04-ddd-tactical-overview.md §6 — Module and project structure mapping

The following shows the overall structure of the 8 projects composing a service and the role of each project.

A service is divided into Src/ (source) and Tests/ (test) folders, consisting of 8 projects total.

{ServiceRoot}/
├── Src/ ← Source projects
│ ├── {ServiceName}/ ← Host (Composition Root)
│ ├── {ServiceName}.Domain/
│ ├── {ServiceName}.Application/
│ ├── {ServiceName}.Adapters.Presentation/
│ ├── {ServiceName}.Adapters.Persistence/
│ └── {ServiceName}.Adapters.Infrastructure/
└── Tests/ ← Test projects
├── {ServiceName}.Tests.Unit/
└── {ServiceName}.Tests.Integration/
#ProjectName PatternSDKRole
1Domain{ServiceName}.DomainMicrosoft.NET.SdkDomain model, Aggregate, Value Object, Port
2Application{ServiceName}.ApplicationMicrosoft.NET.SdkUse cases (Command/Query/EventHandler), external Port
3Adapter: Presentation{ServiceName}.Adapters.PresentationMicrosoft.NET.SdkHTTP endpoints (FastEndpoints)
4Adapter: Persistence{ServiceName}.Adapters.PersistenceMicrosoft.NET.SdkRepository implementation
5Adapter: Infrastructure{ServiceName}.Adapters.InfrastructureMicrosoft.NET.SdkExternal API, Mediator, OpenTelemetry, Pipeline
6Host{ServiceName}Microsoft.NET.Sdk.WebComposition Root (Program.cs)
7Tests.Unit{ServiceName}.Tests.UnitMicrosoft.NET.SdkDomain/Application unit tests
8Tests.Integration{ServiceName}.Tests.IntegrationMicrosoft.NET.SdkHTTP endpoint integration tests
{ServiceName} ← Host
{ServiceName}.Domain ← Domain layer
{ServiceName}.Application ← Application layer
{ServiceName}.Adapters.{Category} ← Adapter layer (Presentation | Persistence | Infrastructure)
{ServiceName}.Tests.Unit ← Unit tests
{ServiceName}.Tests.Integration ← Integration tests

csproj reference example:

<!-- Host → all Adapters + Application -->
<ProjectReference Include="..\LayeredArch.Adapters.Infrastructure\..." />
<ProjectReference Include="..\LayeredArch.Adapters.Persistence\..." />
<ProjectReference Include="..\LayeredArch.Adapters.Presentation\..." />
<ProjectReference Include="..\LayeredArch.Application\..." />
<!-- Adapter → Application (transitively includes Domain) -->
<ProjectReference Include="..\LayeredArch.Application\..." />
<!-- Application → Domain -->
<ProjectReference Include="..\LayeredArch.Domain\..." />

Rules: Dependencies always flow from outside to inside only. Domain references nothing, Application references only Domain, and Adapter references only Application.

The following matrix summarizes which project can reference which project.

From \ ToDomainApplicationPresentationPersistenceInfrastructureHost
Domain
Application
Presentation(transitive)
Persistence(transitive)
Infrastructure(transitive)
Host(transitive)
  • : Direct reference allowed (csproj ProjectReference)
  • : Reference prohibited
  • (transitive): No direct reference; type access via transitive reference through upstream reference
  • : Self

Core Principles:

  1. Domain references nothing — Contains only pure business rules
  2. Application directly references only Domain — Use case orchestration layer
  3. Adapter directly references only Application — Domain is accessed via transitive reference through Application
  4. Cross-references between Adapters are prohibited — Presentation, Persistence, and Infrastructure are independent of each other
  5. Only Host can reference all layers — Composition Root role

Verification: This matrix is automatically verified by LayerDependencyArchitectureRuleTests architecture tests.

Now that we understand the dependency direction and reference rules, let us examine the files common to all projects.

All projects include two common files.

A reference point for assembly scanning. Placed in all projects with the same pattern.

using System.Reflection;
namespace {ServiceName}.{Layer};
public static class AssemblyReference
{
public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly;
}

Namespace examples:

ProjectNamespace
Domain{ServiceName}.Domain
Application{ServiceName}.Application
Adapters.Presentation{ServiceName}.Adapters.Presentation
Adapters.Persistence{ServiceName}.Adapters.Persistence
Adapters.Infrastructure{ServiceName}.Adapters.Infrastructure

Purpose: Used wherever an Assembly reference is needed, such as FluentValidation auto-registration and Mediator handler scanning.

// Usage example — in Infrastructure Registration
services.AddValidatorsFromAssembly(AssemblyReference.Assembly);
services.AddValidatorsFromAssembly(LayeredArch.Application.AssemblyReference.Assembly);

A global using declaration file for each layer. The file name is unified as Using.cs across all projects.

Projectglobal using Contents
DomainLanguageExt, Functorium.Domains.*, own SharedModels
ApplicationLanguageExt, Functorium.Applications.Usecases, FluentValidation, own SharedModels
Adapters.PresentationFastEndpoints, Mediator, LanguageExt.Common
Adapters.PersistenceLanguageExt, Domain Aggregate, own SharedModels
Adapters.InfrastructureFluentValidation, own SharedModels
Complete Using.cs Code by Layer

Domain — Using.cs

global using LanguageExt;
global using LanguageExt.Common;
global using Functorium.Domains.Entities;
global using Functorium.Domains.Events;
global using Functorium.Domains.ValueObjects;
global using Functorium.Domains.ValueObjects.Validations.Typed;
global using LayeredArch.Domain.SharedModels.ValueObjects;

Application — Using.cs

global using LanguageExt;
global using LanguageExt.Common;
global using static LanguageExt.Prelude;
global using Functorium.Applications.Usecases;
global using Functorium.Domains.ValueObjects.Validations.Typed;
global using Functorium.Domains.ValueObjects.Validations.Contextual;
global using FluentValidation;
global using LayeredArch.Domain.SharedModels.ValueObjects;

Adapters.Presentation — Using.cs

global using LanguageExt.Common;
global using FastEndpoints;
global using Mediator;

Adapters.Persistence — Using.cs

global using LanguageExt;
global using LanguageExt.Common;
global using LayeredArch.Domain.AggregateRoots.Products;
global using static LanguageExt.Prelude;
global using LayeredArch.Domain.SharedModels.ValueObjects;

Adapters.Infrastructure — Using.cs

global using FluentValidation;
global using LayeredArch.Domain.SharedModels.ValueObjects;

Each project (layer) has primary and secondary objectives.

  • Primary Objective — The reason the layer exists. Business logic or core technology implementation code is located here.
  • Secondary Objective — Supporting infrastructure for the layer. DI registration, extension methods, etc. are located here.
ProjectPrimary Objective FolderSecondary Objective Folder
DomainAggregateRoots/, SharedModels/, Ports/(none)
ApplicationUsecases/, Ports/(none)
Adapters.PresentationEndpoints/Abstractions/ (Registrations/, Extensions/)
Adapters.PersistenceRepositories/ (InMemory/, EfCore/)Abstractions/ (Options/, Registrations/)
Adapters.InfrastructureExternalApis/, …Abstractions/ (Registrations/)

Secondary objectives of Adapter projects are placed under the Abstractions/ folder.

Abstractions/
├── Options/ ← Adapter configuration options (appsettings.json binding, when needed)
│ └── {Category}Options.cs
├── Registrations/ ← DI service registration extension methods
│ └── Adapter{Category}Registration.cs
└── Extensions/ ← Shared extension methods (when needed)
└── {Name}Extensions.cs
FolderPurposeExample
Options/appsettings.json binding Options classPersistenceOptions, FtpOptions
Registrations/DI service registration extension methodsAdapterPersistenceRegistration
Extensions/Shared extension methodsFinResponseExtensions

Caution: Domain and Application do not have an Abstractions/ folder. See FAQ

If common files form the foundation of a project, the code placement guide determines where new code should be located.

When writing new code, decide “where to place this code?” in 3 steps.

Writing new code
├─ Is it a business rule? → Domain Layer
├─ Is it use case orchestration? → Application Layer
└─ Is it a technical implementation? → Adapter Layer
Code TypeProjectFolder
Entity, Aggregate RootDomainAggregateRoots/{Aggregate}/
Value Object (single Aggregate)DomainAggregateRoots/{Aggregate}/ValueObjects/
Value Object (shared)DomainSharedModels/ValueObjects/
Domain EventDomainAggregateRoots/{Aggregate}/Events/
Domain ServiceDomainSharedModels/Services/
Repository Port (persistence)DomainAggregateRoots/{Aggregate}/Ports/
Cross-Aggregate read-only PortDomainPorts/
Command / QueryApplicationUsecases/{Feature}/
Event HandlerApplicationUsecases/{Feature}/
Application Port (external systems)ApplicationPorts/
HTTP EndpointPresentationEndpoints/{Feature}/
Repository implementationPersistenceRepositories/
Query Adapter implementationPersistenceRepositories/Dapper/
External API serviceInfrastructureExternalApis/
Cross-cutting concerns (Mediator, etc.)InfrastructureAbstractions/Registrations/

For the detailed folder structure of each project, see the Domain Layer, Application Layer, and Adapter Layer sections.

Port interfaces are a frequent decision point, so they are organized separately.

Port interface
├─ Does the method signature use only domain types? → Domain
│ ├─ CRUD specific to a single Aggregate? → AggregateRoots/{Agg}/Ports/
│ └─ Cross-Aggregate read-only? → Ports/ (project root)
└─ Includes external DTOs or technical concerns? → Application/Ports/

For detailed criteria on Port placement, see FAQ: Criteria for Placing Ports in Domain or Application and 12-ports.md.

{ServiceName}.Domain/
├── AggregateRoots/ ← Subfolders per Aggregate Root
├── SharedModels/ ← Cross-Aggregate shared types
├── Ports/ ← Cross-Aggregate Port interfaces
├── AssemblyReference.cs
└── Using.cs

Each Aggregate Root has its own folder, and the internal structure is as follows.

AggregateRoots/
├── Products/
│ ├── Product.cs ← Aggregate Root Entity
│ ├── Entities/ ← Child Entities of this Aggregate (when needed)
│ │ └── ProductVariant.cs
│ ├── Ports/
│ │ └── IProductRepository.cs ← Port specific to this Aggregate
│ ├── Specifications/
│ │ ├── ProductNameUniqueSpec.cs ← Specification specific to this Aggregate
│ │ ├── ProductPriceRangeSpec.cs
│ │ └── ProductLowStockSpec.cs
│ └── ValueObjects/
│ ├── ProductName.cs ← Value Object specific to this Aggregate
│ └── ProductDescription.cs
├── Customers/
│ ├── Customer.cs
│ ├── Ports/
│ │ └── ICustomerRepository.cs
│ ├── Specifications/
│ │ └── CustomerEmailSpec.cs
│ └── ValueObjects/
│ ├── CustomerName.cs
│ └── Email.cs
└── Orders/
├── Order.cs
├── Entities/
│ └── OrderLine.cs ← Child Entity
├── Ports/
│ └── IOrderRepository.cs
└── ValueObjects/
└── ShippingAddress.cs

Rules:

  • The Aggregate Root file ({Aggregate}.cs) is placed at the root of its folder
  • Child Entities of an Aggregate are placed in {Aggregate}/Entities/
  • Ports specific to an Aggregate are placed in {Aggregate}/Ports/
  • Value Objects specific to an Aggregate are placed in {Aggregate}/ValueObjects/
  • Specifications specific to an Aggregate are placed in {Aggregate}/Specifications/

Types shared across multiple Aggregates are placed here.

SharedModels/
├── Entities/
│ └── Tag.cs ← Shared Entity
├── Events/
│ └── TagEvents.cs ← Shared Domain Event
└── ValueObjects/
├── Money.cs ← Shared Value Object
├── Quantity.cs
└── TagName.cs

Ports that do not belong to a single Aggregate and are referenced by other Aggregates are placed in the Ports/ folder at the project root.

Ports/
└── IProductCatalog.cs ← Used by Order for Product verification

Port location decision criteria:

CriteriaLocationExample
CRUD specific to a single AggregateAggregateRoots/{Aggregate}/Ports/IProductRepository
Cross-Aggregate read-onlyPorts/ (project root)IProductCatalog
{ServiceName}.Application/
├── Usecases/ ← Use cases per Aggregate
├── Ports/ ← External system Port interfaces
├── AssemblyReference.cs
└── Using.cs

Organized by Aggregate subfolders.

Usecases/
├── Products/
│ ├── CreateProductCommand.cs
│ ├── UpdateProductCommand.cs
│ ├── DeductStockCommand.cs
│ ├── GetProductByIdQuery.cs
│ ├── GetAllProductsQuery.cs
│ ├── OnProductCreated.cs ← Event Handler
│ ├── OnProductUpdated.cs
│ └── OnStockDeducted.cs
├── Customers/
│ ├── CreateCustomerCommand.cs
│ ├── GetCustomerByIdQuery.cs
│ └── OnCustomerCreated.cs
└── Orders/
├── CreateOrderCommand.cs
├── GetOrderByIdQuery.cs
└── OnOrderCreated.cs

File Naming Rules:

TypePatternExample
Command{Verb}{Aggregate}Command.csCreateProductCommand.cs
Query{Get, etc.}{Description}Query.csGetAllProductsQuery.cs
Event HandlerOn{EventName}.csOnProductCreated.cs
CriteriaDomain PortApplication Port
LocationDomain/AggregateRoots/{Aggregate}/Ports/ or Domain/Ports/Application/Ports/
Implemented byPrimarily Persistence AdapterPrimarily Infrastructure Adapter
RoleDomain object persistence/retrievalExternal system calls (API, messaging, etc.)
ExampleIProductRepository, IProductCatalogIExternalPricingService

Adapters are always split into 3 projects.

ProjectConcernHexagonal RoleRepresentative Folder
Adapters.PresentationHTTP I/ODriving (Outside → Inside)Endpoints/
Adapters.PersistenceData storage/retrievalDriven (Inside → Outside)Repositories/
Adapters.InfrastructureExternal APIs, cross-cutting concerns (Observability, Mediator, etc.)Driven (Inside → Outside)ExternalApis/, …

For the rationale behind the Driving/Driven distinction and the design decision of not having Ports in Presentation, see “Driving vs Driven Adapter Distinction” in 12-ports.md.

Why Primary Objective Folders Are Not Fixed

Section titled “Why Primary Objective Folders Are Not Fixed”

The primary objective folder name of an Adapter varies depending on the implementation technology. Presentation becomes Endpoints/, but could be Services/ for gRPC. Persistence also varies by ORM, such as Repositories/, DbContexts/, etc. Folder names reflect the implementation technology.

{ServiceName}.Adapters.Presentation/
├── Endpoints/
│ ├── Products/
│ │ ├── Dtos/ ← DTOs shared across Endpoints
│ │ │ └── ProductSummaryDto.cs
│ │ ├── CreateProductEndpoint.cs
│ │ ├── UpdateProductEndpoint.cs
│ │ ├── DeductStockEndpoint.cs
│ │ ├── GetProductByIdEndpoint.cs
│ │ └── GetAllProductsEndpoint.cs
│ ├── Customers/
│ │ ├── CreateCustomerEndpoint.cs
│ │ └── GetCustomerByIdEndpoint.cs
│ └── Orders/
│ ├── CreateOrderEndpoint.cs
│ └── GetOrderByIdEndpoint.cs
├── Abstractions/
│ ├── Registrations/
│ │ └── AdapterPresentationRegistration.cs
│ └── Extensions/
│ └── FinResponseExtensions.cs
├── AssemblyReference.cs
└── Using.cs

Endpoints Folder Rules: Subfolders per Aggregate, endpoint file names follow the {Verb}{Aggregate}Endpoint.cs pattern. DTOs shared across multiple Endpoints are placed in a Dtos/ subfolder. Each Endpoint’s Request/Response DTOs are defined as nested records inside the Endpoint class.

{ServiceName}.Adapters.Persistence/
├── Repositories/ ← Subfolders per implementation technology
│ ├── InMemory/ ← InMemory (ConcurrentDictionary) implementation
│ │ ├── Products/
│ │ │ ├── InMemoryProductRepository.cs
│ │ │ ├── InMemoryProductCatalog.cs ← Cross-Aggregate Port implementation
│ │ │ ├── InMemoryProductQuery.cs
│ │ │ ├── InMemoryProductDetailQuery.cs
│ │ │ ├── InMemoryProductWithStockQuery.cs
│ │ │ └── InMemoryProductWithOptionalStockQuery.cs
│ │ ├── Customers/
│ │ │ ├── InMemoryCustomerRepository.cs
│ │ │ ├── InMemoryCustomerDetailQuery.cs
│ │ │ ├── InMemoryCustomerOrderSummaryQuery.cs
│ │ │ └── InMemoryCustomerOrdersQuery.cs
│ │ ├── Orders/
│ │ │ ├── InMemoryOrderRepository.cs
│ │ │ ├── InMemoryOrderDetailQuery.cs
│ │ │ └── InMemoryOrderWithProductsQuery.cs
│ │ ├── Inventories/
│ │ │ ├── InMemoryInventoryRepository.cs
│ │ │ └── InMemoryInventoryQuery.cs
│ │ ├── Tags/
│ │ │ └── InMemoryTagRepository.cs
│ │ └── InMemoryUnitOfWork.cs
│ ├── Dapper/ ← Dapper-based Query Adapter (CQRS Read side)
│ │ ├── DapperProductQuery.cs
│ │ ├── DapperProductWithStockQuery.cs
│ │ ├── DapperProductWithOptionalStockQuery.cs
│ │ ├── DapperInventoryQuery.cs
│ │ ├── DapperCustomerOrderSummaryQuery.cs
│ │ ├── DapperCustomerOrdersQuery.cs
│ │ └── DapperOrderWithProductsQuery.cs
│ └── EfCore/ ← EF Core-based implementation (optional)
│ ├── Models/ ← Persistence Model (POCO, primitive types only)
│ │ ├── ProductModel.cs
│ │ ├── OrderModel.cs
│ │ ├── CustomerModel.cs
│ │ └── TagModel.cs
│ ├── Mappers/ ← Domain ↔ Model conversion (extension methods)
│ │ ├── ProductMapper.cs
│ │ ├── OrderMapper.cs
│ │ ├── CustomerMapper.cs
│ │ └── TagMapper.cs
│ ├── Configurations/
│ │ ├── ProductConfiguration.cs
│ │ ├── OrderConfiguration.cs
│ │ ├── CustomerConfiguration.cs
│ │ └── TagConfiguration.cs
│ ├── {ServiceName}DbContext.cs
│ ├── EfCoreProductRepository.cs
│ ├── EfCoreOrderRepository.cs
│ ├── EfCoreCustomerRepository.cs
│ └── EfCoreProductCatalog.cs
├── Abstractions/
│ ├── Options/ ← Adapter configuration options (optional)
│ │ └── PersistenceOptions.cs
│ └── Registrations/
│ └── AdapterPersistenceRegistration.cs
├── AssemblyReference.cs
└── Using.cs

Note: Repositories/EfCore/ and Abstractions/Options/ are added when using EF Core-based persistence. When using only InMemory, only Repositories/InMemory/ and Abstractions/Registrations/ are needed. When using EF Core, Models/ (Persistence Model) and Mappers/ (Domain ↔ Model conversion) are also added.

{ServiceName}.Adapters.Infrastructure/
├── ExternalApis/
│ └── ExternalPricingApiService.cs ← Application Port implementation
├── Abstractions/
│ └── Registrations/
│ └── AdapterInfrastructureRegistration.cs
├── AssemblyReference.cs
└── Using.cs

DI registration extension methods are placed in the Abstractions/Registrations/ folder of each Adapter.

Registration Method Naming Rules:

MethodPattern
Service registrationRegisterAdapter{Category}(this IServiceCollection)
Middleware configurationUseAdapter{Category}(this IApplicationBuilder)
AdapterPresentationRegistration.cs
public static IServiceCollection RegisterAdapterPresentation(this IServiceCollection services) { ... }
public static IApplicationBuilder UseAdapterPresentation(this IApplicationBuilder app) { ... }
// AdapterPersistenceRegistration.cs — IConfiguration parameter added when using Options pattern
public static IServiceCollection RegisterAdapterPersistence(this IServiceCollection services, IConfiguration configuration) { ... }
public static IApplicationBuilder UseAdapterPersistence(this IApplicationBuilder app) { ... }
// AdapterInfrastructureRegistration.cs
public static IServiceCollection RegisterAdapterInfrastructure(this IServiceCollection services, IConfiguration configuration) { ... }
public static IApplicationBuilder UseAdapterInfrastructure(this IApplicationBuilder app) { ... }

Note: The IConfiguration parameter is required in Adapters that use the Options pattern (RegisterConfigureOptions). For Options pattern details, see 14a-adapter-pipeline-di.md §4.6.

Now that we understand the folder structure of each layer, let us examine the Host project that assembles all layers.

The Host project is the only project that assembles all layers. It uses the Microsoft.NET.Sdk.Web SDK.

var builder = WebApplication.CreateBuilder(args);
// Service registration per layer
builder.Services
.RegisterAdapterPresentation()
.RegisterAdapterPersistence(builder.Configuration)
.RegisterAdapterInfrastructure(builder.Configuration);
// App build and middleware configuration
var app = builder.Build();
app.UseAdapterInfrastructure()
.UseAdapterPersistence()
.UseAdapterPresentation();
app.Run();

Registration Order: Presentation → Persistence → Infrastructure (service registration) Middleware Order: Infrastructure → Persistence → Presentation (middleware configuration)

Service Registration Order (Presentation → Persistence → Infrastructure):

OrderAdapterRationale
1PresentationNo external dependencies (registers only FastEndpoints)
2PersistenceRequires Configuration, registers DB Context/Repository
3InfrastructureRegisters Mediator, Validation, OpenTelemetry, Pipeline — last because Pipeline wraps previously registered Adapters
  • Key point: Infrastructure is last because ConfigurePipelines(p => p.UseObservability().UseValidation().UseException()) activates all Adapter Pipelines registered in previous steps

Middleware Order (Infrastructure → Persistence → Presentation):

OrderAdapterRationale
1InfrastructureObservability middleware — captures all requests/responses from the outermost layer
2PersistenceDB initialization (EnsureCreated)
3PresentationEndpoint mapping (UseFastEndpoints) — innermost layer, handles actual requests
  • Principle: Middleware registered first is positioned on the outer side of the request pipeline
  • File Structure: appsettings.json (default) + appsettings.{Environment}.json (override)
CategoryMethodExample
Configuration branchingappsettings.{Environment}.jsonPersistence.Provider, OpenTelemetry settings
Code branchingapp.Environment.IsDevelopment()Diagnostic endpoints, Swagger
Options patternRegisterConfigureOptions<T, TValidator>()Validation at startup + automatic logging
  • Principle: Use appsettings when branching by configuration values is possible; code branching is used only for code-level differences such as development-only endpoints

Middleware insertion points when adding operational requirements:

1. Exception handling (outermost) — app.UseExceptionHandler()
2. Observability — app.UseAdapterInfrastructure()
3. Security (HTTPS, CORS, Auth) — app.UseHttpsRedirection() / UseCors() / UseAuthentication() / UseAuthorization()
4. Data — app.UseAdapterPersistence()
5. Health Check — app.MapHealthChecks("/health")
6. Endpoints (innermost) — app.UseAdapterPresentation()
  • Note: Currently, exception handling is handled at the Usecase level in the Adapter Pipeline (ExceptionHandlingPipeline). ASP.NET middleware-level exception handling is only needed for infrastructure errors (serialization failures, etc.)

Test projects are placed under the Tests/ folder. For test writing methodology (naming conventions, AAA pattern, MTP settings, etc.), see 15a-unit-testing.md.

Responsible for unit testing the Domain/Application layers.

csproj configuration:

<ItemGroup>
<ProjectReference Include="..\..\Src\{ServiceName}.Domain\{ServiceName}.Domain.csproj" />
<ProjectReference Include="..\..\Src\{ServiceName}.Application\{ServiceName}.Application.csproj" />
<ProjectReference Include="{path}\Functorium.Testing\Functorium.Testing.csproj" />
</ItemGroup>
  • Additional packages: NSubstitute (Mocking)
  • Configuration files: Using.cs, xunit.runner.json

Folder structure:

{ServiceName}.Tests.Unit/
├── Domain/ ← Mirrors Domain layer
│ ├── SharedModels/ ← ValueObject tests
│ ├── {Aggregate}/ ← Aggregate/Entity/ValueObject/Specification tests
│ └── ...
├── Application/ ← Mirrors Application layer
│ ├── {Aggregate}/ ← Usecase handler tests
│ └── ...
├── TestIO.cs ← FinT<IO, T> Mock helper
├── Using.cs
└── xunit.runner.json

TestIO Helper:

A static helper class needed for mocking FinT<IO, T> return values in Application Usecase tests.

internal static class TestIO
{
public static FinT<IO, T> Succ<T>(T value) => FinT.lift(IO.pure(Fin.Succ(value)));
public static FinT<IO, T> Fail<T>(Error error) => FinT.lift(IO.pure(Fin.Fail<T>(error)));
}

xunit.runner.json:

{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeAssembly": false,
"parallelizeTestCollections": true,
"methodDisplay": "method",
"methodDisplayOptions": "replaceUnderscoreWithSpace",
"diagnosticMessages": true
}

Unit tests are Mock-based and each test is independent, so parallelizeTestCollections: true (parallel execution allowed)

Using.cs:

global using Xunit;
global using Shouldly;
global using NSubstitute;
global using LanguageExt;
global using LanguageExt.Common;
global using static LanguageExt.Prelude;

Responsible for integration testing of HTTP endpoints.

csproj configuration:

<ItemGroup>
<ProjectReference Include="..\..\Src\{ServiceName}\{ServiceName}.csproj">
<ExcludeAssets>analyzers</ExcludeAssets>
</ProjectReference>
<ProjectReference Include="..\..\Src\{ServiceName}.Application\{ServiceName}.Application.csproj" />
<ProjectReference Include="{path}\Functorium.Testing\Functorium.Testing.csproj" />
</ItemGroup>
  • Additional packages: Microsoft.AspNetCore.Mvc.Testing
  • Configuration files: Using.cs, xunit.runner.json, appsettings.json

ExcludeAssets=analyzers: When the Host project uses Mediator SourceGenerator, the SourceGenerator also runs in the test project, generating duplicate code. ExcludeAssets=analyzers prevents this.

Folder structure:

{ServiceName}.Tests.Integration/
├── Fixtures/
│ ├── {ServiceName}Fixture.cs ← Inherits HostTestFixture<Program>
│ └── IntegrationTestBase.cs ← IClassFixture + HttpClient provider
├── Endpoints/ ← Mirrors Presentation layer
│ ├── {Aggregate}/
│ │ └── {Endpoint}Tests.cs
│ └── ErrorScenarios/ ← Error handling verification
├── Using.cs
├── xunit.runner.json
└── appsettings.json ← OpenTelemetry settings required

Fixture Pattern:

A two-step pattern that inherits HostTestFixture<Program> to configure a WebApplicationFactory-based test server, and injects HttpClient through IntegrationTestBase.

IntegrationTestBase.cs
// {ServiceName}Fixture.cs
public class {ServiceName}Fixture : HostTestFixture<Program> { }
public abstract class IntegrationTestBase : IClassFixture<{ServiceName}Fixture>
{
protected HttpClient Client { get; }
protected IntegrationTestBase({ServiceName}Fixture fixture) => Client = fixture.Client;
}

xunit.runner.json:

{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeAssembly": false,
"parallelizeTestCollections": false,
"maxParallelThreads": 1,
"methodDisplay": "classAndMethod",
"methodDisplayOptions": "all",
"diagnosticMessages": true
}

Integration tests share In-memory storage, so parallelizeTestCollections: false, maxParallelThreads: 1 (sequential execution required)

appsettings.json:

HostTestFixture runs in the “Test” environment and sets ContentRoot to the test project path. It loads the test project’s appsettings.json instead of the Host project’s appsettings.json, so required settings such as OpenTelemetry must also be placed in the test project.

{
"OpenTelemetry": {
"ServiceName": "{ServiceName}",
"ServiceNamespace": "{ServiceName}",
"CollectorEndpoint": "http://localhost:18889",
"CollectorProtocol": "Grpc",
"TracingEndpoint": "",
"MetricsEndpoint": "",
"LoggingEndpoint": "",
"SamplingRate": 1.0,
"EnablePrometheusExporter": false
}
}

Using.cs:

global using Xunit;
global using Shouldly;
global using System.Net;
global using System.Net.Http.Json;

Namespaces are determined by the project root namespace + folder path.

Folder PathNamespace
Domain/{ServiceName}.Domain
Domain/AggregateRoots/Products/{ServiceName}.Domain.AggregateRoots.Products
Domain/AggregateRoots/Products/Ports/{ServiceName}.Domain.AggregateRoots.Products (Port uses the Aggregate namespace)
Domain/AggregateRoots/Products/Specifications/{ServiceName}.Domain.AggregateRoots.Products.Specifications
Domain/AggregateRoots/Products/ValueObjects/{ServiceName}.Domain.AggregateRoots.Products.ValueObjects
Domain/SharedModels/ValueObjects/{ServiceName}.Domain.SharedModels.ValueObjects
Domain/SharedModels/Services/{ServiceName}.Domain.SharedModels.Services
Domain/Ports/{ServiceName}.Domain.Ports
Application/Usecases/Products/{ServiceName}.Application.Usecases.Products
Application/Ports/{ServiceName}.Application.Ports
Adapters.Presentation/Endpoints/Products/{ServiceName}.Adapters.Presentation.Endpoints.Products
Adapters.Presentation/Abstractions/Registrations/{ServiceName}.Adapters.Presentation.Abstractions.Registrations
Adapters.Persistence/Repositories/InMemory/{ServiceName}.Adapters.Persistence.Repositories.InMemory
Adapters.Persistence/Repositories/Dapper/{ServiceName}.Adapters.Persistence.Repositories.Dapper
Adapters.Persistence/Repositories/EfCore/{ServiceName}.Adapters.Persistence.Repositories.EfCore
Adapters.Persistence/Repositories/EfCore/Configurations/{ServiceName}.Adapters.Persistence.Repositories.EfCore.Configurations
Adapters.Persistence/Abstractions/Registrations/{ServiceName}.Adapters.Persistence.Abstractions.Registrations
Adapters.Infrastructure/ExternalApis/{ServiceName}.Adapters.Infrastructure.ExternalApis
Adapters.Infrastructure/Abstractions/Registrations/{ServiceName}.Adapters.Infrastructure.Abstractions.Registrations
Tests.Unit/Domain/SharedModels/{ServiceName}.Tests.Unit.Domain.SharedModels
Tests.Unit/Domain/{Aggregate}/{ServiceName}.Tests.Unit.Domain.{Aggregate}
Tests.Unit/Application/{Aggregate}/{ServiceName}.Tests.Unit.Application.{Aggregate}
Tests.Integration/Fixtures/{ServiceName}.Tests.Integration.Fixtures
Tests.Integration/Endpoints/{Aggregate}/{ServiceName}.Tests.Integration.Endpoints.{Aggregate}
  1. Domain project

    • Create {ServiceName}.Domain project (SDK: Microsoft.NET.Sdk)
    • Add AssemblyReference.cs
    • Add Using.cs
    • Create AggregateRoots/ folder
    • Create SharedModels/ folder (when needed)
    • Create Ports/ folder (when cross-Aggregate Ports exist)
  2. Application project

    • Create {ServiceName}.Application project
    • Add AssemblyReference.cs
    • Add Using.cs
    • Create Usecases/ folder
    • Create Ports/ folder (when external system Ports exist)
    • Add Domain project reference
  3. Adapters.Presentation project

    • Create {ServiceName}.Adapters.Presentation project
    • Add AssemblyReference.cs
    • Add Using.cs
    • Create Endpoints/ folder
    • Add Abstractions/Registrations/AdapterPresentationRegistration.cs
    • Add Application project reference
  4. Adapters.Persistence project

    • Create {ServiceName}.Adapters.Persistence project
    • Add AssemblyReference.cs
    • Add Using.cs
    • Create Repositories/ folder
    • Add Abstractions/Registrations/AdapterPersistenceRegistration.cs
    • Add Application project reference
  5. Adapters.Infrastructure project

    • Create {ServiceName}.Adapters.Infrastructure project
    • Add AssemblyReference.cs
    • Add Using.cs
    • Add Abstractions/Registrations/AdapterInfrastructureRegistration.cs
    • Add Application project reference
  6. Host project

    • Create {ServiceName} project (SDK: Microsoft.NET.Sdk.Web)
    • Add references to all Adapter + Application projects
    • Program.cs — Add layer registration method calls
  7. Tests.Unit project

    • Create {ServiceName}.Tests.Unit project
    • Add Using.cs
    • Add xunit.runner.json (parallelizeTestCollections: true)
    • Add TestIO.cs helper
    • Add Domain + Application + Functorium.Testing references
    • Create Domain/ folder structure (source mirroring)
    • Create Application/ folder structure (source mirroring)
  8. Tests.Integration project

    • Create {ServiceName}.Tests.Integration project
    • Add Using.cs
    • Add xunit.runner.json (parallelizeTestCollections: false, maxParallelThreads: 1)
    • Add appsettings.json (OpenTelemetry settings)
    • Add Host(ExcludeAssets=analyzers) + Application + Functorium.Testing references
    • Create Fixtures/ folder (Fixture + IntegrationTestBase)
    • Create Endpoints/ folder structure (Presentation mirroring)

When Circular References Occur Between Projects

Section titled “When Circular References Occur Between Projects”

Cause: Occurs when there are cross-references between Adapter projects, or when Domain/Application references an Adapter.

Solution:

  1. Check the dependency direction matrix — dependencies must always flow from outside to inside only
  2. Run LayerDependencyArchitectureRuleTests architecture tests to identify violation points
  3. Move types that need to be shared to an inner layer (Domain or Application)

Cause: It is difficult to determine whether a Value Object will be used by multiple Aggregates.

Solution:

  1. Initially place it in AggregateRoots/{Aggregate}/ValueObjects/
  2. When a reference from another Aggregate becomes necessary, move it to SharedModels/ValueObjects/
  3. Since the namespace changes when moving, update the global using in Using.cs

When Mediator SourceGenerator Duplication Error Occurs in Integration Tests

Section titled “When Mediator SourceGenerator Duplication Error Occurs in Integration Tests”

Cause: When the Tests.Integration project references the Host project, the SourceGenerator also runs in the test project.

Solution:

<ProjectReference Include="..\..\Src\{ServiceName}\{ServiceName}.csproj">
<ExcludeAssets>analyzers</ExcludeAssets>
</ProjectReference>

The Domain layer has no secondary objectives. This is because Domain contains only pure business rules and has no infrastructure concerns such as DI registration or framework configuration. Application also has no Abstractions for the same reason.

Why Adapter Primary Objective Folder Names Are Not Fixed

Section titled “Why Adapter Primary Objective Folder Names Are Not Fixed”

The primary objective folder name of an Adapter varies depending on the implementation technology. For example, if Presentation uses FastEndpoints it becomes Endpoints/, and if it uses gRPC it becomes Services/. In contrast, the secondary objective folder (Abstractions/) always uses the same name regardless of technology.

Criteria for Placing Value Objects Between SharedModels and AggregateRoots

Section titled “Criteria for Placing Value Objects Between SharedModels and AggregateRoots”
  • Used by only one AggregateAggregateRoots/{Aggregate}/ValueObjects/
    • Example: ProductName, ProductDescriptionProducts/ValueObjects/
  • Shared across multiple AggregatesSharedModels/ValueObjects/
    • Example: Money, QuantitySharedModels/ValueObjects/

Initially place as Aggregate-specific, and move to SharedModels when sharing becomes necessary.

Criteria for Placing Ports in Domain or Application

Section titled “Criteria for Placing Ports in Domain or Application”
  • Domain object persistence/retrieval → Domain’s AggregateRoots/{Aggregate}/Ports/ or Ports/
    • Example: IProductRepository, IProductCatalog
  • External system integration → Application’s Ports/
    • Example: IExternalPricingService

Key criterion: If the interface’s method signatures use only domain types, place it in Domain; if they include external DTOs or technical concerns, place it in Application.

Why Observability Settings Go in Infrastructure

Section titled “Why Observability Settings Go in Infrastructure”

Observability (OpenTelemetry, Serilog, etc.) is a cross-cutting concern that does not belong to a specific Adapter category. It is placed here because the Infrastructure Adapter is responsible for comprehensively managing cross-cutting concerns such as Mediator, Validator, OpenTelemetry, and Pipeline.

Why ExcludeAssets=analyzers Is Needed When Referencing Host in Integration Tests

Section titled “Why ExcludeAssets=analyzers Is Needed When Referencing Host in Integration Tests”

When the Host project uses Mediator SourceGenerator, the SourceGenerator also runs in the test project, generating duplicate code. ExcludeAssets=analyzers prevents this.

Why appsettings.json Is Needed in Integration Tests

Section titled “Why appsettings.json Is Needed in Integration Tests”

HostTestFixture sets ContentRoot to the test project path. It loads the test project’s appsettings.json instead of the Host project’s appsettings.json, so required settings such as OpenTelemetry must also be placed in the test project.

Why Parallel Execution Settings Differ Between Unit and Integration Tests

Section titled “Why Parallel Execution Settings Differ Between Unit and Integration Tests”

Unit tests are Mock-based and each test is independent, so parallel execution is possible. Integration tests share In-memory storage, so they run sequentially to prevent state interference between tests.