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:

Project네임스페이스
Domain{ServiceName}.Domain
Application{ServiceName}.Application
Adapters.Presentation{ServiceName}.Adapters.Presentation
Adapters.Persistence{ServiceName}.Adapters.Persistence
Adapters.Infrastructure{ServiceName}.Adapters.Infrastructure

Purpose: FluentValidation 자동 등록, Mediator 핸들러 스캔 등 Assembly 참조가 필요한 곳에서 사용합니다.

// 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 내용
DomainLanguageExt, Functorium.Domains.*, 자체 SharedModels
ApplicationLanguageExt, Functorium.Applications.Usecases, FluentValidation, 자체 SharedModels
Adapters.PresentationFastEndpoints, Mediator, LanguageExt.Common
Adapters.PersistenceLanguageExt, Domain Aggregate, 자체 SharedModels
Adapters.InfrastructureFluentValidation, 자체 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.
Project주 목표 폴더부수 목표 폴더
DomainAggregateRoots/, SharedModels/, Ports/(없음)
ApplicationUsecases/, Ports/(없음)
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 구성 옵션 (appsettings.json 바인딩, 필요 시)
│ └── {Category}Options.cs
├── Registrations/ ← DI 서비스 등록 확장 메서드
│ └── Adapter{Category}Registration.cs
└── Extensions/ ← 공유 확장 메서드 (필요 시)
└── {Name}Extensions.cs
FolderPurposeExample
Options/appsettings.json 바인딩 Options 클래스PersistenceOptions, FtpOptions
Registrations/DI 서비스 등록 확장 메서드AdapterPersistenceRegistration
Extensions/공유 확장 메서드FinResponseExtensions

Caution: Domain과 Application에는 Abstractions/ 폴더가 없습니다. 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.

새 코드 작성
├─ 비즈니스 규칙인가? → Domain Layer
├─ 유스케이스 조율인가? → Application Layer
└─ 기술적 구현인가? → Adapter Layer
Code TypeProjectFolder
Entity, Aggregate RootDomainAggregateRoots/{Aggregate}/
Value Object (단일 Aggregate)DomainAggregateRoots/{Aggregate}/ValueObjects/
Value Object (공유)DomainSharedModels/ValueObjects/
Domain EventDomainAggregateRoots/{Aggregate}/Events/
Domain ServiceDomainSharedModels/Services/
Repository Port (영속성)DomainAggregateRoots/{Aggregate}/Ports/
교차 Aggregate 읽기 전용 PortDomainPorts/
Command / QueryApplicationUsecases/{Feature}/
Event HandlerApplicationUsecases/{Feature}/
Application Port (외부 시스템)ApplicationPorts/
HTTP EndpointPresentationEndpoints/{Feature}/
Repository implementation체PersistenceRepositories/
Query Adapter 구현체PersistenceRepositories/Dapper/
외부 API 서비스InfrastructureExternalApis/
횡단 관심사 (Mediator 등)InfrastructureAbstractions/Registrations/

각 프로젝트의 상세 폴더 구조는 Domain 레이어, Application 레이어, Adapter 레이어 섹션.

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

Port 인터페이스
├─ 메서드 시그니처가 도메인 타입만 사용? → Domain
│ ├─ 특정 Aggregate 전용 CRUD? → AggregateRoots/{Agg}/Ports/
│ └─ 교차 Aggregate 읽기 전용? → Ports/ (프로젝트 루트)
└─ 외부 DTO나 기술적 관심사 포함? → Application/Ports/

Port 배치의 상세 기준은 FAQ §Port를 Domain에 둘지 Application에 둘지12-ports.md를 참조하세요.

{ServiceName}.Domain/
├── AggregateRoots/ ← Aggregate Root별 하위 폴더
├── SharedModels/ ← 교차 Aggregate 공유 타입
├── Ports/ ← 교차 Aggregate Port 인터페이스
├── 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/ ← 이 Aggregate의 자식 Entity (필요 시)
│ │ └── ProductVariant.cs
│ ├── Ports/
│ │ └── IProductRepository.cs ← 이 Aggregate 전용 Port
│ ├── Specifications/
│ │ ├── ProductNameUniqueSpec.cs ← 이 Aggregate 전용 Specification
│ │ ├── ProductPriceRangeSpec.cs
│ │ └── ProductLowStockSpec.cs
│ └── ValueObjects/
│ ├── ProductName.cs ← 이 Aggregate 전용 Value Object
│ └── ProductDescription.cs
├── Customers/
│ ├── Customer.cs
│ ├── Ports/
│ │ └── ICustomerRepository.cs
│ ├── Specifications/
│ │ └── CustomerEmailSpec.cs
│ └── ValueObjects/
│ ├── CustomerName.cs
│ └── Email.cs
└── Orders/
├── Order.cs
├── Entities/
│ └── OrderLine.cs ← 자식 Entity
├── Ports/
│ └── IOrderRepository.cs
└── ValueObjects/
└── ShippingAddress.cs

Rules:

  • Aggregate Root 파일({Aggregate}.cs)은 해당 폴더의 루트에 배치
  • Aggregate의 자식 Entity는 {Aggregate}/Entities/ 에 배치
  • Aggregate 전용 Port는 {Aggregate}/Ports/ 에 배치
  • Aggregate 전용 Value Object는 {Aggregate}/ValueObjects/ 에 배치
  • Aggregate 전용 Specification은 {Aggregate}/Specifications/ 에 배치

Types shared across multiple Aggregates are placed here.

SharedModels/
├── Entities/
│ └── Tag.cs ← 공유 Entity
├── Events/
│ └── TagEvents.cs ← 공유 Domain Event
└── ValueObjects/
├── Money.cs ← 공유 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 ← Order에서 Product 검증용으로 사용

Port 위치 결정 기준:

CriteriaLocationExample
특정 Aggregate 전용 CRUDAggregateRoots/{Aggregate}/Ports/IProductRepository
교차 Aggregate 읽기 전용Ports/ (프로젝트 루트)IProductCatalog
{ServiceName}.Application/
├── Usecases/ ← Aggregate별 유스케이스
├── Ports/ ← 외부 시스템 Port 인터페이스
├── 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{동사}{Aggregate}Command.csCreateProductCommand.cs
Query{Get 등}{설명}Query.csGetAllProductsQuery.cs
Event HandlerOn{Event명}.csOnProductCreated.cs
CriteriaDomain PortApplication Port
LocationDomain/AggregateRoots/{Aggregate}/Ports/ 또는 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.

ProjectConcern헥사고날 Role대표 폴더
Adapters.PresentationHTTP 입출력Driving (Outside → Inside)Endpoints/
Adapters.Persistence데이터 저장/조회Driven (Inside → Outside)Repositories/
Adapters.Infrastructure외부 API, 횡단 관심사(Observability, Mediator 등)Driven (Inside → Outside)ExternalApis/, …

Driving/Driven 구분과 Presentation에 Port가 없는 설계 결정의 근거는 12-ports.md의 “Driving vs Driven Adapter 구분” 참조.

Primary Objective Folders가 고정되지 않는 이유

Section titled “Primary Objective Folders가 고정되지 않는 이유”

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/ ← Endpoint 간 공유 DTO
│ │ │ └── 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/ ← 구현 기술별 하위 폴더
│ ├── InMemory/ ← InMemory(ConcurrentDictionary) 구현
│ │ ├── Products/
│ │ │ ├── InMemoryProductRepository.cs
│ │ │ ├── InMemoryProductCatalog.cs ← 교차 Aggregate Port 구현
│ │ │ ├── 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 기반 Query Adapter (CQRS Read 측)
│ │ ├── DapperProductQuery.cs
│ │ ├── DapperProductWithStockQuery.cs
│ │ ├── DapperProductWithOptionalStockQuery.cs
│ │ ├── DapperInventoryQuery.cs
│ │ ├── DapperCustomerOrderSummaryQuery.cs
│ │ ├── DapperCustomerOrdersQuery.cs
│ │ └── DapperOrderWithProductsQuery.cs
│ └── EfCore/ ← EF Core 기반 구현 (선택)
│ ├── Models/ ← Persistence Model (POCO, primitive 타입만)
│ │ ├── ProductModel.cs
│ │ ├── OrderModel.cs
│ │ ├── CustomerModel.cs
│ │ └── TagModel.cs
│ ├── Mappers/ ← 도메인 ↔ Model 변환 (확장 메서드)
│ │ ├── 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 구성 옵션 (선택)
│ │ └── PersistenceOptions.cs
│ └── Registrations/
│ └── AdapterPersistenceRegistration.cs
├── AssemblyReference.cs
└── Using.cs

Note: Repositories/EfCore/Abstractions/Options/는 EF Core 기반 영속화를 사용할 때 추가합니다. InMemory만 사용하는 경우 Repositories/InMemory/Abstractions/Registrations/만 있으면 됩니다. EF Core 사용 시 Models/(Persistence Model)과 Mappers/(도메인 ↔ Model 변환)가 함께 추가됩니다.

{ServiceName}.Adapters.Infrastructure/
├── ExternalApis/
│ └── ExternalPricingApiService.cs ← Application Port 구현
├── 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
서비스 등록RegisterAdapter{Category}(this IServiceCollection)
미들웨어 설정UseAdapter{Category}(this IApplicationBuilder)
AdapterPresentationRegistration.cs
public static IServiceCollection RegisterAdapterPresentation(this IServiceCollection services) { ... }
public static IApplicationBuilder UseAdapterPresentation(this IApplicationBuilder app) { ... }
// AdapterPersistenceRegistration.cs — Options 패턴 사용 시 IConfiguration 파라미터 추가
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: IConfiguration 파라미터는 Options 패턴(RegisterConfigureOptions)을 사용하는 Adapter에서 필요합니다. Options 패턴 상세는 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. SDK는 Microsoft.NET.Sdk.Web을 사용합니다.

var builder = WebApplication.CreateBuilder(args);
// 레이어별 서비스 등록
builder.Services
.RegisterAdapterPresentation()
.RegisterAdapterPersistence(builder.Configuration)
.RegisterAdapterInfrastructure(builder.Configuration);
// App 빌드 및 미들웨어 설정
var app = builder.Build();
app.UseAdapterInfrastructure()
.UseAdapterPersistence()
.UseAdapterPresentation();
app.Run();

Registration Order: Presentation → Persistence → Infrastructure (서비스 등록) Middleware Order: Infrastructure → Persistence → Presentation (미들웨어 설정)

Service Registration Order (Presentation → Persistence → Infrastructure):

OrderAdapterRationale
1Presentation외부 의존성 없음 (FastEndpoints만 등록)
2PersistenceConfiguration 필요, DB Context/Repository 등록
3InfrastructureMediator, Validation, OpenTelemetry, Pipeline 등록 — Pipeline이 앞서 등록된 Adapter를 래핑하므로 마지막
  • 핵심: Infrastructure가 마지막인 이유는 ConfigurePipelines(p => p.UseObservability().UseValidation().UseException())이 이전 단계에서 등록된 모든 Adapter Pipeline을 활성화하기 때문

미들웨어 순서 (Infrastructure → Persistence → Presentation):

OrderAdapterRationale
1Infrastructure관찰성 미들웨어 — 가장 바깥쪽에서 모든 요청/응답 캡처
2PersistenceDB 초기화 (EnsureCreated)
3Presentation엔드포인트 매핑 (UseFastEndpoints) — 가장 안쪽, 실제 요청 처리
  • 원칙: 먼저 등록된 미들웨어가 요청 파이프라인의 바깥쪽에 위치
  • File Structure: appsettings.json (기본) + appsettings.{Environment}.json (오버라이드)
Category방법Example
설정값 분기appsettings.{Environment}.jsonPersistence.Provider, OpenTelemetry 설정
코드 분기app.Environment.IsDevelopment()진단 엔드포인트, Swagger
Options 패턴RegisterConfigureOptions<T, TValidator>()시작 시 검증 + 자동 로깅
  • 원칙: 설정값으로 분기 가능하면 appsettings 사용, 코드 분기는 개발 전용 엔드포인트 등 코드 레벨 차이에만 사용

운영 요구사항 추가 시 미들웨어 삽입 위치:

① 예외 처리 (가장 바깥쪽) — app.UseExceptionHandler()
② 관찰성 — app.UseAdapterInfrastructure()
③ 보안 (HTTPS, CORS, 인증) — app.UseHttpsRedirection() / UseCors() / UseAuthentication() / UseAuthorization()
④ 데이터 — app.UseAdapterPersistence()
⑤ Health Check — app.MapHealthChecks("/health")
⑥ 엔드포인트 (가장 안쪽) — app.UseAdapterPresentation()
  • 참고: 현재 예외 처리는 Adapter Pipeline(ExceptionHandlingPipeline)에서 Usecase 레벨로 처리. ASP.NET 미들웨어 레벨 예외 처리는 인프라 오류(직렬화 실패 등)에만 필요

Test projects are placed under the Tests/ folder. 테스트 작성 방법론(명명 규칙, AAA 패턴, MTP 설정 등)은 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>
  • 추가 패키지: NSubstitute (Mocking)
  • 구성 파일: Using.cs, xunit.runner.json

Folder structure:

{ServiceName}.Tests.Unit/
├── Domain/ ← Domain 레이어 미러링
│ ├── SharedModels/ ← ValueObject 테스트
│ ├── {Aggregate}/ ← Aggregate/Entity/ValueObject/Specification 테스트
│ └── ...
├── Application/ ← Application 레이어 미러링
│ ├── {Aggregate}/ ← Usecase 핸들러 테스트
│ └── ...
├── TestIO.cs ← FinT<IO, T> Mock 헬퍼
├── Using.cs
└── xunit.runner.json

TestIO Helper:

Application Usecase 테스트에서 FinT<IO, T> 반환값 Mock에 필요한 정적 헬퍼 클래스입니다.

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
}

단위 테스트는 Mock 기반으로 각 테스트가 독립적이므로 parallelizeTestCollections: true (병렬 허용)

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>
  • 추가 패키지: Microsoft.AspNetCore.Mvc.Testing
  • 구성 파일: Using.cs, xunit.runner.json, appsettings.json

ExcludeAssets=analyzers: Host 프로젝트가 Mediator SourceGenerator를 사용하는 경우, 테스트 프로젝트에서도 SourceGenerator가 실행되어 중복 코드가 생성됩니다. ExcludeAssets=analyzers로 이를 방지합니다.

Folder structure:

{ServiceName}.Tests.Integration/
├── Fixtures/
│ ├── {ServiceName}Fixture.cs ← HostTestFixture<Program> 상속
│ └── IntegrationTestBase.cs ← IClassFixture + HttpClient 제공
├── Endpoints/ ← Presentation 레이어 미러링
│ ├── {Aggregate}/
│ │ └── {Endpoint}Tests.cs
│ └── ErrorScenarios/ ← 에러 처리 검증
├── Using.cs
├── xunit.runner.json
└── appsettings.json ← OpenTelemetry 설정 필수

Fixture Pattern:

HostTestFixture<Program>을 상속하여 WebApplicationFactory 기반 테스트 서버를 구성하고, IntegrationTestBase를 통해 HttpClient를 주입하는 2단계 패턴입니다.

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
}

통합 테스트는 In-memory 저장소를 공유하므로 parallelizeTestCollections: false, maxParallelThreads: 1 (순차 실행 Required)

appsettings.json:

HostTestFixture는 “Test” 환경으로 실행하며 ContentRoot를 테스트 프로젝트 경로로 설정합니다. Host 프로젝트의 appsettings.json이 아닌 테스트 프로젝트의 appsettings.json을 로드하므로, OpenTelemetry 등 Required 설정을 테스트 프로젝트에도 배치해야 합니다.

{
"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.

폴더 경로네임스페이스
Domain/{ServiceName}.Domain
Domain/AggregateRoots/Products/{ServiceName}.Domain.AggregateRoots.Products
Domain/AggregateRoots/Products/Ports/{ServiceName}.Domain.AggregateRoots.Products (Port는 Aggregate 네임스페이스)
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 프로젝트

    • {ServiceName}.Domain 프로젝트 생성 (SDK: Microsoft.NET.Sdk)
    • AssemblyReference.cs 추가
    • Using.cs 추가
    • AggregateRoots/ 폴더 생성
    • SharedModels/ 폴더 생성 (필요 시)
    • Ports/ 폴더 생성 (교차 Aggregate Port가 있을 경우)
  2. Application 프로젝트

    • {ServiceName}.Application 프로젝트 생성
    • AssemblyReference.cs 추가
    • Using.cs 추가
    • Usecases/ 폴더 생성
    • Ports/ 폴더 생성 (외부 시스템 Port가 있을 경우)
    • Domain 프로젝트 참조 추가
  3. Adapters.Presentation 프로젝트

    • {ServiceName}.Adapters.Presentation 프로젝트 생성
    • AssemblyReference.cs 추가
    • Using.cs 추가
    • Endpoints/ 폴더 생성
    • Abstractions/Registrations/AdapterPresentationRegistration.cs 추가
    • Application 프로젝트 참조 추가
  4. Adapters.Persistence 프로젝트

    • {ServiceName}.Adapters.Persistence 프로젝트 생성
    • AssemblyReference.cs 추가
    • Using.cs 추가
    • Repositories/ 폴더 생성
    • Abstractions/Registrations/AdapterPersistenceRegistration.cs 추가
    • Application 프로젝트 참조 추가
  5. Adapters.Infrastructure 프로젝트

    • {ServiceName}.Adapters.Infrastructure 프로젝트 생성
    • AssemblyReference.cs 추가
    • Using.cs 추가
    • Abstractions/Registrations/AdapterInfrastructureRegistration.cs 추가
    • Application 프로젝트 참조 추가
  6. Host 프로젝트

    • {ServiceName} 프로젝트 생성 (SDK: Microsoft.NET.Sdk.Web)
    • 모든 Adapter + Application 프로젝트 참조 추가
    • Program.cs — 레이어 등록 메서드 호출 추가
  7. Tests.Unit 프로젝트

    • {ServiceName}.Tests.Unit 프로젝트 생성
    • Using.cs 추가
    • xunit.runner.json 추가 (parallelizeTestCollections: true)
    • TestIO.cs 헬퍼 추가
    • Domain + Application + Functorium.Testing 참조 추가
    • Domain/ 폴더 구조 생성 (소스 미러링)
    • Application/ 폴더 구조 생성 (소스 미러링)
  8. Tests.Integration 프로젝트

    • {ServiceName}.Tests.Integration 프로젝트 생성
    • Using.cs 추가
    • xunit.runner.json 추가 (parallelizeTestCollections: false, maxParallelThreads: 1)
    • appsettings.json 추가 (OpenTelemetry 설정)
    • Host(ExcludeAssets=analyzers) + Application + Functorium.Testing 참조 추가
    • Fixtures/ 폴더 생성 (Fixture + IntegrationTestBase)
    • Endpoints/ 폴더 구조 생성 (Presentation 미러링)

When Circular References Occur Between Projects

Section titled “When Circular References Occur Between Projects”

Cause: Adapter 프로젝트 간 상호 참조 또는 Domain/Application이 Adapter를 참조하는 경우 발생합니다.

Solution:

  1. 의존성 방향 매트릭스를 확인합니다 — 의존성은 항상 바깥에서 안쪽으로만 향해야 합니다
  2. LayerDependencyArchitectureRuleTests 아키텍처 테스트를 실행하여 위반 지점을 확인합니다
  3. 공유가 필요한 타입은 더 안쪽 레이어(Domain 또는 Application)로 이동합니다

Cause: Value Object가 여러 Aggregate에서 사용되는지 판단이 어려운 경우입니다.

Solution:

  1. 처음에는 AggregateRoots/{Aggregate}/ValueObjects/에 배치합니다
  2. 다른 Aggregate에서 참조가 필요해지면 SharedModels/ValueObjects/로 이동합니다
  3. 이동 시 네임스페이스가 변경되므로 Using.cs의 global using을 업데이트합니다

When Mediator SourceGenerator Duplication Error Occurs in Integration Tests

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

Cause: Tests.Integration 프로젝트가 Host 프로젝트를 참조하면서 SourceGenerator가 테스트 프로젝트에서도 실행됩니다.

Solution:

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

Domain 레이어에는 부수 목표가 없습니다. Domain은 순수한 비즈니스 규칙만 포함하며, DI 등록이나 프레임워크 설정 같은 인프라 관심사가 존재하지 않기 때문입니다. Application도 동일한 이유로 Abstractions가 없습니다.

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. 예를 들어 Presentation이 FastEndpoints를 사용하면 Endpoints/, gRPC를 사용하면 Services/가 됩니다. 반면 부수 목표 폴더(Abstractions/)는 기술과 무관하게 항상 같은 이름을 사용합니다.

Criteria for Placing Value Objects Between SharedModels and AggregateRoots

Section titled “Criteria for Placing Value Objects Between SharedModels and AggregateRoots”
  • 하나의 Aggregate에서만 사용AggregateRoots/{Aggregate}/ValueObjects/
    • 예: ProductName, ProductDescriptionProducts/ValueObjects/
  • 여러 Aggregate에서 공유SharedModels/ValueObjects/
    • 예: 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의 AggregateRoots/{Aggregate}/Ports/ 또는 Ports/
    • 예: IProductRepository, IProductCatalog
  • 외부 시스템 통합 → Application의 Ports/
    • 예: IExternalPricingService

핵심 기준: 인터페이스의 메서드 시그니처가 도메인 타입만 사용하면 Domain, 외부 DTO나 기술적 관심사를 포함하면 Application에 배치합니다.

Infrastructure에 Observability 설정이 들어가는 이유

Section titled “Infrastructure에 Observability 설정이 들어가는 이유”

Observability(OpenTelemetry, Serilog 등)는 횡단 관심사로, 특정 Adapter 카테고리에 속하지 않습니다. Infrastructure Adapter가 Mediator, Validator, OpenTelemetry, Pipeline 등 횡단 관심사를 종합적으로 관리하는 Role을 담당하기 때문에 이곳에 배치합니다.

통합 테스트에서 Host 참조 시 ExcludeAssets=analyzers가 필요한 이유

Section titled “통합 테스트에서 Host 참조 시 ExcludeAssets=analyzers가 필요한 이유”

Host 프로젝트가 Mediator SourceGenerator를 사용하는 경우, 테스트 프로젝트에서도 SourceGenerator가 실행되어 중복 코드가 생성됩니다. ExcludeAssets=analyzers로 이를 방지합니다.

통합 테스트에 appsettings.json이 필요한 이유

Section titled “통합 테스트에 appsettings.json이 필요한 이유”

HostTestFixture는 ContentRoot를 테스트 프로젝트 경로로 설정합니다. Host 프로젝트의 appsettings.json이 아닌 테스트 프로젝트의 appsettings.json을 로드하므로, OpenTelemetry 등 Required 설정을 테스트 프로젝트에도 배치해야 합니다.

단위 테스트와 통합 테스트의 병렬 실행 설정이 다른 이유

Section titled “단위 테스트와 통합 테스트의 병렬 실행 설정이 다른 이유”

단위 테스트는 Mock 기반으로 각 테스트가 독립적이므로 병렬 실행이 가능합니다. 통합 테스트는 In-memory 저장소를 공유하므로 테스트 간 상태 간섭을 방지하기 위해 순차 실행합니다.