Skip to main content

vertical-slice-architecture

Scaffold and navigate a .NET Clean Architecture codebase organised as Vertical Slice Architecture (VSA) — the Hona/VerticalSliceArchitecture layout of Domain / Common / Features slices with FastEndpoints, EF Core, Vogen strong IDs, Mapperly, FluentValidation, and Testcontainers. Use this skill whenever the user mentions vertical slice architecture, VSA, clean architecture, CQRS commands and queries, feature folders, feature slices, "add an endpoint / use case / command / query", FastEndpoints, Vogen value objects, strong-typed IDs, Mapperly mappers, architecture tests, or asks where a piece of code belongs in a layered .NET API — even if they never say "vertical slice". Also use it before adding any new entity, value object, DbContext configuration, or API operation to such a repo, because each of those has non-obvious wiring in other files that is easy to miss.

来源信息

仓库
thangchung/agent-engineering-experiment
最近来源活动
2026年9月24日 13:42
检测到的 SKILL.md 语言
英语
星标
27
分支
6

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
3 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
vertical-slice-architecture
description
Scaffold and navigate a .NET Clean Architecture codebase organised as Vertical Slice Architecture (VSA) — the Hona/VerticalSliceArchitecture layout of Domain / Common / Features slices with FastEndpoints, EF Core, Vogen strong IDs, Mapperly, FluentValidation, and Testcontainers. Use this skill whenever the user mentions vertical slice architecture, VSA, clean architecture, CQRS commands and queries, feature folders, feature slices, "add an endpoint / use case / command / query", FastEndpoints, Vogen value objects, strong-typed IDs, Mapperly mappers, architecture tests, or asks where a piece of code belongs in a layered .NET API — even if they never say "vertical slice". Also use it before adding any new entity, value object, DbContext configuration, or API operation to such a repo, because each of those has non-obvious wiring in other files that is easy to miss.
# Vertical Slice Architecture (Clean Architecture, sliced) This skill encodes the structure of the `Hona/VerticalSliceArchitecture` template so you can scaffold components correctly the first time and reason about an unfamiliar repo quickly. The single most valuable thing here is the **wiring map**: in this architecture most additions touch more than one file, and the extra files are not discoverable by reading the file you edit. Skipping them produces code that compiles and fails at runtime. Read `references/wiring.md` before finishing any scaffolding task. ## Why this layout exists Classic Clean Architecture cuts the codebase **horizontally** — one project per ring, and an interface at every ring boundary (`IGameRepository`, `IDateTimeProvider`, …). The cost is that a single feature is smeared across four projects, and most of those interfaces have exactly one implementation forever. VSA keeps Clean Architecture's **dependency rule** (dependencies point inward, `Domain` at the centre, and nothing in `Domain` knows about the web or the database) and deletes the **project-per-ring plus interface-per-boundary** ceremony. One use case is one file. Coupling is cut per feature instead of per layer. Practical consequence: `AppDbContext` is injected straight into an endpoint. That is deliberate, not a mistake — do not "fix" it by introducing a repository interface unless the user asks. ### Clean Architecture ring → VSA location | Clean Architecture ring | Lives in | May depend on | | --- | --- | --- | | Entities / domain model | `Domain/` | nothing (only BCL + Vogen/Guard attributes) | | Use cases / application logic | body of `HandleAsync` in the slice file | `Domain`, `Common` | | Interface adapters (controller, presenter, DTOs) | same slice file: `Configure()`, `Request`, plus `Features/{Plural}/Common/{Entity}Response.cs` | `Domain`, `Common` | | Infrastructure / frameworks | `Common/EfCore/`, `Program.cs` | `Domain` | Because the use case and its adapter sit in the same file, a slice is read top-to-bottom: DTO, validator, route, logic. That is the property that makes this layout fast to navigate. ## Dependency model ```mermaid graph RL Host["Host — Program.cs"] Games["Features/Games — slice group"] Players["Features/Players — slice group"] Common["Common/ — EfCore, cross-cutting"] Domain["Domain/ — entities, value objects"] Host --> Games Host --> Players Host --> Common Games --> Common Players --> Common Games --> Domain Players --> Domain Common --> Domain Games -.->|"forbidden"| Players linkStyle 8 stroke:#c0392b,stroke-width:2px ``` Five invariants the architecture tests enforce. Treat them as hard constraints while scaffolding: 1. **`Domain` depends on nothing.** No EF Core, no ASP.NET, no `Common`, no `Features`. 2. **`Common` depends only on `Domain`.** It never reaches into `Features`. 3. **Slice groups never depend on each other.** If `Features/Games` needs something from `Features/Players`, that something belongs in `Domain` or `Common` — move it, do not import across groups. This is the invariant most often broken by accident. 4. **Use cases are `internal sealed`, CQRS-named** (`*Command` for writes, `*Query` for reads), and each has a sibling `Request` and, unless it returns `Created`/`NoContent`, a `Response`. 5. **Every entity's ID is a Vogen value object**, never a bare `Guid`/`int`. ## Directory map ``` src/{App}/ ├── Program.cs host: DI, FastEndpoints, Swagger, migrations │ also holds [assembly: VogenDefaults(...)] │ ends with `public partial class Program;` (tests need it) ├── GlobalUsings.cs why slice files have almost no `using` lines ├── Domain/ dependency-free core │ ├── {Entity}.cs entity: private EF ctor + public ctor + behaviour methods │ ├── {Entity}Id.cs [ValueObject<Guid>] strong ID │ ├── {Concept}.cs value objects, enums │ └── {Rule}Exception.cs domain exceptions ├── Common/ infrastructure + cross-cutting, one level above Domain │ └── EfCore/ │ ├── AppDbContext.cs DbSet per aggregate root; applies configs by assembly scan │ ├── EfCoreConverters.cs [EfCoreConverter<T>] per Vogen ID — easy to forget │ ├── DependencyInjectionExtensions.cs │ ├── Configuration/{Entity}Configuration.cs │ └── Migrations/ └── Features/ one folder per slice group (plural noun) └── {Plural}/ ├── {Verb}{Entity}Command.cs one write use case, whole file ├── {Verb}{Entity}Query.cs one read use case, whole file └── Common/ shared *within this group only* └── {Entity}Response.cs response DTO + [Mapper] Mapperly class tests/ ├── {App}.Unit.Tests/Domain/ entity behaviour, no host, no DB ├── {App}.Integration.Tests/ real host + Testcontainers Postgres, per-slice tests │ ├── IntegrationTestBase.cs │ ├── TestAppFactory.cs │ └── Features/{Plural}/{UseCase}Tests.cs └── {App}.Architecture.Tests/ the five invariants above ``` ### Naming convention Derived from the template's own `dotnet new` symbols. Stay consistent — the architecture tests and the source generators both key off these shapes. | Symbol | Example | Used for | | --- | --- | --- | | `{Entity}` | `Game` | entity type, DTO prefix, config class | | `{Plural}` | `Games` | `Features/Games/`, `DbSet<Game> Games` | | `{pluralCamel}` | `games` | route segment: `/games`, `/games/{GameId}/play-turn` | | `{Verb}{Entity}` | `PlayTurn`, `NewGame`, `ViewGame` | use-case class name stem | ## Scaffolding decision flow ```mermaid flowchart TD Start["What is being added?"] --> Sol{"Whole solution?"} Sol -->|yes| CLI["dotnet new install Hona.VerticalSliceArchitecture.Template<br/>then dotnet new hona-vsa"] Sol -->|no| Op{"An HTTP operation?"} Op -->|"changes state"| Cmd["Features/Plural/VerbEntityCommand.cs"] Op -->|"reads only"| Qry["Features/Plural/VerbEntityQuery.cs"] Op -->|no| Concept{"A domain concept?"} Concept -->|"has identity + lifecycle"| Ent["Domain/Entity.cs plus Domain/EntityId.cs"] Concept -->|"value with constraints"| VO["Domain/Concept.cs as Vogen ValueObject"] Concept -->|"a rule violation"| Exc["Domain/RuleException.cs"] Concept -->|no| Shared{"Shared by 2+ slice groups?"} Shared -->|yes| Cm["Common/ — never Features/"] Shared -->|"no, one group only"| Grp["Features/Plural/Common/"] Ent --> Wire["MANDATORY: references/wiring.md<br/>DbSet + EfCoreConverter + Configuration"] VO --> Wire Cmd --> Check["Validator? Response DTO? Integration test?"] Qry --> Check Check --> Wire ``` `dotnet new hona-vsa` already scaffolds the solution and `dotnet new hona-vsa-slice --featureName X` scaffolds a slice group. Prefer those over hand-writing boilerplate when the template is installed; hand-write single slices, since a slice is one file. ## Request lifecycle ```mermaid sequenceDiagram participant C as Client participant FE as FastEndpoints participant V as RequestValidator participant UC as Use case endpoint participant E as Domain entity participant DB as AppDbContext participant M as Mapperly C->>FE: POST /games/{GameId}/play-turn FE->>FE: bind route + body into Request, Vogen IDs parsed FE->>V: validate Request V-->>C: 400 with errors, short-circuits FE->>UC: HandleAsync request, cancellationToken UC->>DB: FindAsync entity by strong ID DB-->>UC: entity or null UC-->>C: 404 when null UC->>E: call behaviour method E->>E: enforce invariants, throw domain exception if broken UC->>DB: SaveChangesAsync cancellationToken UC->>M: ToResponse on the loaded entity M-->>UC: Response DTO UC-->>C: TypedResults.Ok response ``` The read path differs in one important way: a query never loads an entity and maps it. It projects in the database via `ProjectToResponse()` on `IQueryable<T>` with `AsNoTracking()`, so only the response columns are fetched. Using `ToResponse()` in a query is a correctness-neutral but real performance bug — see `references/templates.md`. ## Slice anatomy One use case, one file, in this order. The order matters for readability, which is the point of the architecture. ``` namespace {App}.Features.{Plural}; public sealed record {Verb}{Entity}Request(...) // public: tests construct it internal sealed class {Verb}{Entity}RequestValidator // optional; auto-discovered : AbstractValidator<{Verb}{Entity}Request> internal sealed class {Verb}{Entity}Command(AppDbContext db) : Endpoint<{Verb}{Entity}Request, Results<Ok<{Entity}Response>, NotFound>> Configure() // verb + route + auth + Swagger summary HandleAsync() // the entire use case ``` Pseudocode for the two shapes. Port these to any stack; the branch structure is the contract. ``` COMMAND (writes): entity <- db.FindAsync<Entity>(request.StrongId, ct) if entity is null: return NotFound entity.BehaviourMethod(request...) # invariants live in Domain, not here db.SaveChangesAsync(ct) return Ok(entity.ToResponse()) # in-memory map, entity already loaded QUERY (reads): response <- db.{Plural} .AsNoTracking() .Where(x => x.Id == request.StrongId) .ProjectToResponse() # projection pushed into SQL .FirstOrDefaultAsync(ct) if response is null: return NotFound return Ok(response) CREATE (writes, no response body): entity <- new Entity(EntityId.FromNewGuid(), request...) db.Add(entity); db.SaveChangesAsync(ct) return Created("/{pluralCamel}/" + entity.Id) ``` Two rules for where logic goes, both from the template's own guidance: - **Start in the slice.** Write "just get it working" code in `HandleAsync`. Promote logic into `Domain` only when a second use case needs it. Premature domain modelling is the main failure mode here. - **Invariants are the exception.** Anything that must hold for the entity to be valid belongs in the entity from the start, so no slice can bypass it. ## Wiring checklist Each line is a dependency that is invisible from the file you are editing. Full explanations, failure symptoms, and code in `references/wiring.md` — read it, do not work from memory. - Adding a **Vogen strong ID** → also add `[EfCoreConverter<TId>]` in `Common/EfCore/EfCoreConverters.cs`, or EF Core cannot persist it. - Using **`Id.FromNewGuid()`** → requires `[assembly: VogenDefaults(customizations: Customizations.AddFactoryMethodForGuids)]` in `Program.cs`. - Adding an **aggregate root** → also add `DbSet<T>` to `AppDbContext` and `Common/EfCore/Configuration/{Entity}Configuration.cs`. - Mapping a **collection or array property** → the `HasConversion` needs a `ValueComparer`, or EF Core silently misses changes and `SaveChangesAsync` writes nothing. - Adding an **endpoint** → discovery is source-generated; new endpoints appear via `DiscoveredTypes.All` and need no registration, but they do need `FastEndpoints.Generator` referenced and a successful build. - Adding an **integration test** → `Program.cs` must keep `public partial class Program;`. - Throwing a **domain exception** from a slice → decide its HTTP mapping explicitly; the template ships no global handler, so an uncaught domain exception becomes a 500. ## Verify before reporting done ```bash dotnet build # source generators run here; endpoint + mapper errors surface dotnet test tests/{App}.Architecture.Tests # the five invariants dotnet test # integration tests need Docker for Testcontainers ``` A change is not finished until `dotnet build` is clean: FastEndpoints, Vogen, and Mapperly are all source generators, so a large class of mistakes in this architecture is invisible until build time and does not appear in the editor. ## References - `references/templates.md` — copy-ready code for every component: entity, value object, strong ID, EF configuration, command, query, validator, Mapperly response, and the three test kinds. Read when writing any new file. - `references/wiring.md` — the dependency and correlation map: what to touch when adding each component, why, and the runtime symptom when it is missed. Read before finishing any scaffolding task. </content> </invoke>
在 GitHub 查看