--- name: dotnet-api-architecture description: "Use when adding, moving, renaming, or registering files, folders, or projects in a .NET solution shaped Api → Service → Repository → Entity → Common (+ Dto), or when asked where a type goes or what to call it: placement, one type per file (Models/, Exceptions/ subfolders), type and DI names, options and validators, services over provider-owned persistence (no repositories), and the test layout." --- # .NET API architecture Where a file goes and what it is called in a minimal-API solution shaped `.Api → .Service → .Repository → .Entity`, plus `.Dto` and `.Common`, tested by `.Unit.Test` and `.Integration.Test`. `Service` owns the services, which query the `DbContext` directly — there are no repository classes; `Repository` owns persistence plumbing only. This skill fixes placement and names. Code shape is `csharp-standards`, endpoint shape `aspnet-rest-apis`, mapping practice `ef-core` (base entities `ef-core-base-entities`, lookup tables `ef-core-enum-reference-tables`), the test stack `csharp-xunit`. Record a solution's own folder map, adopted names, and sanctioned deviations in its `AGENTS.md` sections (`## Layout`, `## Conventions`), not here. Placeholders: `` = solution prefix (`Contoso.Shop`); `` = plural noun (`Orders`, `Documents`); `` = singular (`Order`); `` = a persistence technology (`Sql`, `Mongo`, `Blob`); `` = gerund or mass noun for a sub-pipeline a feature owns (`Extraction`, `Tokenization`); `` = an options section (`Export`, `Telemetry`); `` = one fixed product word on infrastructure DI methods and the DbContext (`AddContoso…`, `ContosoDbContext`). ## Layering - Project references: `Api → Service → Repository → Entity → Common`, `Dto → Common`, and `Service → Dto`. `Entity` and `Repository` never reference `Dto`; `Common` references no project; `Entity` takes no EF Core package — every mapping lives in `Repository//Configurations/`. `Service` reaches the EF Core API through its reference to `Repository`, which is where the `DbContext` lives. - **The composition root declares what it names.** `Api` carries a `ProjectReference` to every project whose types appear in its source — commonly `Service`, `Repository`, and `Dto` — because it registers them. Transitive flow is never relied on to make a type compile. `Add` in `Api/Configuration/` registers a feature's services; `Repository` cannot see `Service`, so a provider's `AddPersistence` registers plumbing only. - No `Abstractions` project, no AutoMapper. Interfaces live with their implementations and exist so tests can substitute them; mapping is hand-written static classes. - **A `Service` type is never named after a storage technology.** `` words appear only inside `Repository//`. A service is named for what it does (`OrderService`, `CartService`, `SessionCache`), not for the store behind it (never `SqlOrderService` or `RedisCartService`). Service uses the `DbContext` and the provider-agnostic EF Core API; provider types, client construction, and connection strings stay in `Repository//`. ## Rules that apply everywhere - **Namespace == path under `RootNamespace`.** `.Service/Documents/Extraction/X.cs` declares `namespace .Service.Documents.Extraction;`. Moving a file changes its namespace and nothing else. `Program.cs` declares no namespace. Enforce it in `.editorconfig` (`references/naming.md`). - **No loose `.cs` at a project root.** Every type is inside a folder. - **Folder names:** collections of like types are plural (`Endpoints`, `Options`, `Configurations`, `Models`, `Exceptions`, ``); techniques and infrastructure are a gerund or mass noun (`Extraction`, `Caching`, `Middleware`, `Provisioning`); persistence providers carry the technology's proper name (`Sql`, `Mongo`). - **One type per file, kind-first subfolders.** A leaf folder keeps its working types at its root — services, mappers, endpoint modules, handlers, filters, middleware, static helpers with method bodies — and sorts everything else into subfolders, one type per file: `Models/` for classes, records, structs, and record structs; `Exceptions/` for `Exception.cs`; `Constants/` for const-only static holders shared inside the folder (a const used by one type is a `private const` on that type; solution-wide catalogs stay in `Common/Constants/`); `Interfaces/` per the rule below. No `Records.cs`, `Classes.cs`, `Structs.cs`, `Constants.cs`, or `Exceptions.cs` grouping files. Nested and private types stay nested; enums are never grouped and live in `Common/Enums/`. - **File name == type name, with two sanctioned exceptions:** an interface with its single implementation in one file named after the implementation, interface first; a validator in the file of the type it validates. - **Interfaces with two or more implementations, or whose implementations live in a subfolder**, go to `/Interfaces/I.cs`, one per file. - **Services own data access.** `Service//Service.cs` takes the `DbContext` through its primary constructor (parameter `ctx`, field `_ctx`), queries it directly, and is registered by `Add`. The `DbContext` is already the unit of work and each `DbSet` a repository, so there is no `IRepository` / `Repository`, no generic `IRepository`, and no unit-of-work wrapper — in any project. - **Options.** `Options` classes, each with `public const string SectionName`, live in the `Options/` folder of the lowest project that reads them — `Api/Options/` for host concerns, `Service/Options/` for business knobs, `Repository//Options/` for store settings, never a shared one at the Repository root. Binding and validation: `references/options-and-validation.md`. - **Validators** (FluentValidation only, per `csharp-standards`): one `AbstractValidator` per validated type, declared in the same file as that type. - **Enums** all live in `Common/Enums/`, one per file, named in the plural (`OrderStatuses`) so they never collide with an entity or an enum reference table (`OrderStatus`); a wire-name companion is `Names` in the owning Service feature. **Constants** (string and Guid catalogs, const-only) all live in `Common/Constants/`. Dto, Entity, Service, and Api hold no enums and no catalogs. - **Exceptions** are one per file in the `Exceptions/` subfolder of the folder whose code throws them (`Service//Exceptions/Exception.cs`). `Service/Exceptions/` holds only the three solution-wide types: `NotFoundException`, `ForbiddenException`, `ConflictException`. A feature exception derives from the shared type whose status it means (`OrderClosedException : ConflictException`). A store fault never reaches Api untranslated: the service in `Service//` catches `DbUpdateConcurrencyException`, `DbUpdateException`, and provider exceptions and throws the feature's domain exception or one of the three shared types, so Api's `GlobalExceptionHandler` names no EF Core, provider, or feature type. - **Tests mirror source.** `test/.Unit.Test///Tests.cs` (ProjectShortName ∈ Api, Service, Repository, Entity, Dto, Common); integration tests by feature folder plus `Endpoints/`, `Health/`, `Middleware/`. `TestInfrastructure/` at each test project root holds every fixture, fake, builder, and collection definition — no helper types beside tests, no `*Tests` class inside it. A behaviour-named file (`Tests.cs`) is allowed only when there is no single subject type. This skill is authoritative for the test-project layout; `csharp-xunit` for the test stack. - **Shipped assets move with their code** (prompt templates, fonts): the csproj item and the `AppContext.BaseDirectory` constant that reads it change in the same commit; keep the output path with `Link` when the source folder moves. ## Solution map ```text .Api Program.cs · Configuration/ (+Models/, +Providers/) · Endpoints/ · ExceptionHandlers/ · Filters/ · Health/ · Middleware/ · Observability/ · Options/ · Problems/ · Startup/ .Service / (+Models/, +Exceptions/, +Constants/, +Interfaces/, +/) · BackgroundJobs/ · Caching/ · Exceptions/ · Observability/ · Options/ · Prompts/ · Security/ · Serialization/ · Sorting/ .Repository / only — DbContexts/ · Configurations/ (+Base/) · Migrations/ · Scripts/ · Interceptors/ · Options/ · HealthChecks/ · Provisioning/ · Serialization/ · Models/ .Entity Base/ (BaseEntity, BaseCreatedEntity, BaseModifiedEntity, BaseEnumEntity) · / — plain classes, one file per entity, no mapping attributes .Dto Actions// (one action DTO + its validator per file) · / (one response DTO per file) · Pagination/ .Common Constants/ · Enums/ · Extensions/ · Validation/ test/.Unit.Test Api/ Service/ Repository/ Entity/ Dto/ Common/ · TestInfrastructure/ test/.Integration.Test Endpoints/ · / · Health/ · Middleware/ · TestInfrastructure/ ``` Full trees and what each folder holds: `references/project-layout.md`. ## Decision table — "You are adding…" | You are adding… | It goes in… | Named… | |---|---|---| | an endpoint module | `Api/Endpoints/` | `Endpoints.cs`, `MapEndpoints` — the `Endpoints` of `aspnet-rest-apis`, which fixes the handler shape | | a DI registration for a feature | `Api/Configuration/` | `Configuration.cs`, `Add(this IServiceCollection, IConfiguration)` — registers the feature's services; drop the `IConfiguration` parameter when the feature binds no options | | a DI registration for infrastructure | `Api/Configuration/`, or the owning `Api/{Health,Middleware,Observability,Problems}/` | `Configuration.cs` / `Registration.cs`, `Add` | | a per-provider registration for a model or external API | `Api/Configuration/Providers/` | `ProviderConfiguration.cs`, `AddProvider` | | **a persistence provider** | `Repository//` | the technology's proper name — `Sql/`, `Mongo/` | | **a persistence registration** | `Repository//` | `PersistenceConfiguration.cs`, `AddPersistence`, called from `Program.cs`; registers the DbContext, interceptors, options, and health probe — never a service; `Api/Configuration/` holds no store wiring | | **a store health probe** | `Repository//HealthChecks/` | `HealthCheck.cs`, internal, exposed through `AddHealthCheck`; the name, tag, and route stay in `Api/Health/HealthRegistration.cs` | | **a store provisioner or schema migrator** | `Repository//Provisioning/` | `ResourceProvisioner.cs` / `SchemaMigrator.cs`, public; run by an `Api/Startup/Bootstrapper.cs` | | an options class read only by Api | `Api/Options/` | `Options.cs` with `SectionName` + validator | | an options class read by Service | `Service/Options/` | `Options.cs` with `SectionName` + validator | | **an options class read by a store** | `Repository//Options/` | `DbOptions.cs` with `SectionName` + validator | | **a validator** | the file of the type it validates | `Validator : AbstractValidator<>` | | a service | `Service//` | `Service.cs` (`IService` first); injects the `DbContext` as `ctx` and queries it; translates store faults; registered by `Add` | | an interface with one implementation | the implementation's file | `I` above `` | | an interface with 2+ implementations, or one whose implementations live in a subfolder | `/Interfaces/` | `I.cs` | | a mapper | `Service//` | `Mapper.cs`, static, `MapToDto` + `MapToDtoExpression` | | a request DTO or its validator | `Dto/Actions//` | `CreateActionDto.cs` holding `CreateActionDto` + `CreateActionDtoValidator`; `UpdateActionDto.cs`; `ListActionDto.cs` for query parameters (`[AsParameters]`) — one DTO per file | | a response DTO | `Dto//` | `Dto.cs`, `Dto.cs` — one per file | | an entity | `Entity//` | `.cs` — a plain class deriving from the shallowest `Entity/Base/` class that fits (`ef-core-base-entities`); every mapping lives in its EF configuration | | **a base entity** | `Entity/Base/` | `BaseEntity`, `BaseCreatedEntity`, `BaseModifiedEntity` (`ef-core-base-entities`); `BaseEnumEntity` (`ef-core-enum-reference-tables`) | | **an enum reference table** | `Entity//` | `.cs`, sealed, `: BaseEnumEntity<>` (`OrderStatus : BaseEnumEntity`) | | an EF configuration | `Repository//Configurations//` | `Configuration.cs` deriving from the matching `BaseConfiguration` — keys, column lengths and precision, row version, keyless, relationships, indexes, check constraints, conversions, seed data (`ef-core`) | | **a shared EF base configuration** | `Repository//Configurations/Base/` | `BaseConfiguration`, abstract; `Configure` maps the base then calls `ConfigureEntity` | | a migration | `Repository//Migrations/` | `dotnet ef migrations add ` | | **a SQL script (view, procedure, seed)** | `Repository//Scripts/` | `.sql`, an `` named after the migration that runs it through `migrationBuilder.Sql(Scripts.Read(""))` | | **an EF interceptor** | `Repository//Interceptors/` | `Interceptor.cs`, internal, one per file; attached in `PersistenceConfiguration.cs` | | **a non-EF store client** | `Repository//` | `Store.cs` (`IStore` first) — a thin gateway over the client so no provider type reaches Service; no business queries or domain rules | | an enum | `Common/Enums/` | `.cs`, plural | | an enum's wire names | `Service//` | `Names.cs`, static | | a constant catalog | `Common/Constants/` | `.cs` (`PermissionIds`, `TelemetryNames`) | | a record / struct / non-service class | the leaf folder's `Models/` | `.cs`, one per file | | a folder-local constant holder | the leaf folder's `Constants/` | `.cs`; a const one type uses is a `private const` on that type | | a feature exception | `Service//Exceptions/` (or `/Exceptions/`) | `Exception.cs`, one per file, deriving from the shared type whose status it means | | a store fault (EF Core or provider exception) | caught in `Service//Service.cs` | rethrown as the feature's `Exceptions/Exception` or `NotFoundException` / `ForbiddenException` / `ConflictException` | | a solution-wide exception | `Service/Exceptions/` | only `NotFoundException`, `ForbiddenException`, `ConflictException` (409, row-version conflicts) | | a static helper with method bodies | beside its callers | own file, named for what it does (`Sql.cs`, `Calculator.cs`) | | a background job | `Service/BackgroundJobs/` (shared) or `Service//` (feature-owned) | `Processor.cs`, `Queue.cs` (`Channel` when CA1711 rejects a public `…Queue`) | | a cache | `Service/Caching/` | `Cache.cs` with `ICache`; `CacheOptions` in `Service/Options/` | | a shipped asset (prompt, template, font) | beside the code that reads it | kebab-case file; csproj `` + `Link`; `AppContext.BaseDirectory` constant | | a middleware | `Api/Middleware/` | `Middleware.cs` + `Registration.cs` | | an endpoint filter | `Api/Filters/` | `EndpointFilter.cs`; the validation filter of `aspnet-rest-apis` lives here | | the exception handler | `Api/ExceptionHandlers/` | `GlobalExceptionHandler.cs`, the one `IExceptionHandler` (`aspnet-rest-apis` `references/exception-handling.md`); a new exception derives from a shared type instead of adding a handler | | a health check that is not a store probe | `Api/Health/` | `HealthCheck.cs`; registered in `HealthRegistration.cs` | | a startup validator | `Api/Startup/` | `Bootstrapper.cs` | | a unit test | `test/.Unit.Test///` | `Tests.cs` | | an integration test | `test/.Integration.Test//` or `Endpoints/` | `IntegrationTests.cs` | | test infrastructure | `test/.*.Test/TestInfrastructure/` | `Fixture.cs`, `Fake.cs` implementing `I`, `Collection.cs` | ## Never Only the bans not already stated as rules above. - No repository classes (`IRepository`, `IRepository`), unit-of-work wrappers, or `Repositories/` folders over an EF Core `DbContext` — the service queries the context. - No `Helpers/`, `Utils/`, `Tool/`, `Settings/`, or `Mappers/` folders. - No `*Settings` classes, no `Configure` — options bind through `AddOptions()` (`references/options-and-validation.md`). - No shared `Options/` or `Serialization/` folder at the Repository root when provider folders exist — each provider owns its own. - No mapping attributes on an entity (no DataAnnotations, no `[Timestamp]`) and no constants class for column lengths or precision — they live in the `IEntityTypeConfiguration`; no model-owned (`HasData`) seed for rows operators change after release — write those as `InsertData` in the migration that creates the table, or as a `Scripts/` file that migration runs. - Never regenerate a migration to absorb a CLR rename; edit the type-name strings and verify with `dotnet ef migrations has-pending-model-changes`. - Framework bans (controllers, validation attributes, OpenAPI tooling) are not restated here; `csharp-standards` and `aspnet-rest-apis` own them. ## References Read these on demand — they are not loaded until you need them. | Read this | When | | --- | --- | | `references/project-layout.md` | Scaffolding a solution, project, provider folder, or feature end to end; the thing you are adding is not in the decision table; you need to know what a folder holds before touching it | | `references/naming.md` | Naming a type, file, folder, DI method, test, or fake; the two file-name exceptions and the kind-first subfolders; reviewing names; the `.editorconfig` lines that enforce namespace == folder | | `references/options-and-validation.md` | Adding or binding an options class, placing or registering a validator, or a validator that never runs | | `references/persistence.md` | Adding a persistence provider, EF configuration or base configuration, interceptor, migration, SQL script, provisioner, or store health probe; querying the store from a service; adding a second store; a store fault reaching Api |