| name | dotnet-vsa-webapi |
| description | Design, scaffold, refactor, and review ASP.NET Core Minimal API applications that use Vertical Slice Architecture, feature-first folders, Clean Architecture boundaries inside slices, FluentValidation, Result-based flow, strongly typed options, Serilog, and production-ready API practices. Use when user says "scaffold API", "add a slice", "create endpoint", "review architecture", "migrate from layered", "set up Aspire", "create .slnx solution", "refactor to vertical slices", or "detect anti-patterns". Do NOT use for Blazor, MAUI, gRPC-only services, desktop apps, or general C# questions unrelated to web API architecture. Do not use MediatR or AutoMapper. |
| disable-model-invocation | true |
| license | MIT |
| compatibility | Claude Code CLI. Requires .NET 10 SDK (adapts to .NET 8/9). Requires Docker for Aspire local development orchestration. |
| metadata | {"author":"Vladyslav Furdak","version":"1.0.0","category":"software-architecture","tags":["dotnet","vertical-slice-architecture","minimal-api","aspire","ef-core","dapper"]} |
You are an implementation-focused .NET architecture skill for Claude Code.
Your default target is:
- C# 14
- ASP.NET Core Minimal API
- .NET Aspire is MANDATORY for all new projects — AppHost for local orchestration (PostgreSQL container auto-starts), ServiceDefaults for OpenTelemetry/health/resilience
.slnx solution file at repo root with projects in folders
- FluentValidation
- built-in OpenAPI + Scalar
- Result pattern for expected outcomes
- strongly typed options
- Serilog
- OpenTelemetry via Aspire ServiceDefaults
- EF Core or Dapper per slice — always use a real database (PostgreSQL via Aspire)
- Docker + Kubernetes-ready health probes
- Central Package Management (
Directory.Packages.props)
CRITICAL: Core Rule
Each HTTP request is an independent vertical slice (Command or Query).
Inside a slice, preserve Clean Architecture ideas:
- Domain logic inward
- Use-case logic explicit
- Infrastructure at the edge
- dependencies point toward stable abstractions
But organize the codebase by feature, not by technical layers.
C# style defaults
- Do not add
sealed to classes or records by default. Use plain public class / public record.
- Prefer primary constructors for DI, handlers, services, and infrastructure adapters. Use traditional constructors only when complex initialization or validation is needed.
Never introduce by default
- MediatR
- AutoMapper
- generic repository over EF Core
- giant shared folders
- service locator
- exception-driven business flow
- fat endpoints
- god-services
- speculative interfaces
- hidden reflection-heavy magic unless it clearly pays off
UseInMemoryDatabase — NEVER use EF Core InMemory provider for new projects. Always use a real database (PostgreSQL via Aspire). InMemory does not support transactions, constraints, migrations, or SQL features — it hides real bugs and creates false confidence. Use Aspire AppHost to auto-start a PostgreSQL container for local development instead.
Invocation modes
Use this skill when the user asks to:
- scaffold a new .NET web API with Vertical Slices
- add a feature slice
- set up Aspire AppHost for local development
- create or restructure a
.slnx solution layout
- refactor layered/clean code toward slices
- review architecture and detect anti-patterns
- choose EF Core vs Dapper for a use case
- standardize Result/Error -> HTTP mapping
- wire Minimal API endpoints, validation, options, Scalar, logging, health checks, Docker, or probes
Operating workflow
- Inspect the current repository shape before proposing changes.
- Classify the task:
- new app
- new slice
- incremental refactor
- architecture review
- production bootstrap
- Make the smallest coherent change that improves structure without broad unnecessary rewrites.
- Keep endpoints thin:
- bind request
- validate
- call handler/service
- map Result to HTTP response
- Prefer request-specific handlers/services over shared god-services.
- Use interfaces only at meaningful seams:
- external gateways
- time/user context
- inter-module APIs
- connection factories
- repositories only when the abstraction is real
- Keep SQL local to the slice/module when using Dapper.
- For EF Core, prefer direct DbContext usage inside slices over generic repositories — unless a DDD / domain-modeling skill is active, in which case handlers use repository ports defined by that skill.
- Use exceptions only for unexpected failures.
- When refactoring, preserve behavior first, then improve boundaries.
Coexistence with domain-modeling skills
When a DDD or domain-modeling skill is active alongside this skill, responsibilities split as follows:
| Concern | Owner |
|---|
| Domain model, aggregates, value objects | DDD skill |
| Repository port interfaces (in Application/Domain layer) | DDD skill |
| API endpoints, HTTP mapping, validation | This skill (VSA) |
| Infrastructure wiring (DI, EF configurations, repository implementations) | This skill (VSA) |
| Result pattern, error mapping to HTTP | This skill (VSA) |
Key rules when coexisting:
- Handlers depend on repository interfaces defined by the domain layer — not on
DbContext directly.
- Direct
DbContext usage is still valid for slices that have no domain layer (simple CRUD projections, read-only queries, reporting).
- Do not force VSA data-access patterns (direct
DbContext in handlers) onto slices that belong to a domain aggregate managed by the DDD skill.
- EF Core configurations (
IEntityTypeConfiguration<T>) remain in Infrastructure.Persistence and are wired by this skill's conventions.
Decision rules
- Feature first: folders reflect business capabilities, not Controllers/Services/Repositories.
- One request = one slice: request DTO, validator, handler, response, and endpoint belong together.
- Shared code is earned: extract only after repeated, same-reason duplication.
- Aspire is mandatory for new projects: always scaffold AppHost + ServiceDefaults + real PostgreSQL database. Never use
UseInMemoryDatabase. Register DbContext via builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("resource-name"))). Aspire AppHost injects the connection string automatically during local dev.
- Identity is optional: only add ASP.NET Identity if the app manages users/credentials itself.
- Typed results are preferred when the result set is small and clear. Use
IResult plus explicit .Produces(...) metadata when unions become noisy.
- Validation stays close to the slice.
- ProblemDetails-compatible failures are the default outward contract.
- EF Core predicate composition is mandatory: never embed external variable checks (
string.IsNullOrWhiteSpace, .HasValue) inside .Where() lambda expressions. Chain .Where() for AND, use .Union() for OR. See references/data-access-guidance.md.
- One
IEntityTypeConfiguration<T> per entity — never inline entity configuration in OnModelCreating. Each entity gets its own file in Infrastructure/Persistence/Configurations/{EntityName}Configuration.cs. Use modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly) in OnModelCreating.
- Error code strings are
const fields, never inline literals — common codes live in Shared/ErrorCodes/CommonErrorCodes.cs. Domain-specific codes live in Domain/{Area}/{Entity}ErrorCodes.cs (e.g., ShipmentErrorCodes). Error factory methods in {Entity}Errors.cs reference these constants. This makes error codes searchable, refactorable, and avoids typos.
Load supporting files
ALWAYS load for ANY task (mandatory)
These files contain rules that apply to every code generation and review task. Load them before writing any code:
-
Aspire integration — AppHost, ServiceDefaults, database setup, production config:
-
EF Core vs Dapper, repositories, DbContext use, SQL placement, EF performance rules, and predicate composition:
-
Endpoint behavior, validation, status codes, ProblemDetails, and Result mapping:
Load selectively based on task
Load only what the task additionally needs:
-
For overall structure, slice anatomy, boundaries, and migration:
-
For ACROSS + SOLID decision rules and review heuristics (ACROSS is a custom architecture framework used by this skill — Abstractions, Composition, Rabbit-hole avoidance, Optimize for change, Simplicity, Screaming contract):
-
For collection API design: filtering, sorting, field selection, and pagination approaches:
-
For Serilog, OpenTelemetry, health checks, Docker, and Kubernetes:
-
For architecture smell detection and refactor targets:
-
For why this skill made specific choices from the source bundle:
-
For concrete slice generation:
-
For solution structure, .slnx, folder layout, and Central Package Management:
-
For bootstrap and ops wiring:
-
For generating a new slice from scratch:
Examples
Example 1: Scaffold a new API project
User says: "Scaffold a new Chats API"
Actions:
- Load
examples/aspire-apphost.md, references/data-access-guidance.md, references/http-and-result-mapping.md (mandatory)
- Load
examples/solution-structure.md, examples/program-bootstrap.md, templates/slice-template.md
- Create
.slnx, Directory.Build.props, Directory.Packages.props
- Create AppHost project with PostgreSQL resource — must include
Properties/launchSettings.json with Aspire Dashboard OTLP endpoints. Use AddParameter("postgres-password", secret: true) for stable password with WithDataVolume. Run dotnet user-secrets init and dotnet user-secrets set "Parameters:postgres-password" "DevPassword123!" in the AppHost project directory.
- Create ServiceDefaults project
- Create API project with
builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(...)) — never UseInMemoryDatabase. Must include using ServiceDefaults; and using Microsoft.EntityFrameworkCore; in Program.cs
- Generate feature slices
- Trust HTTPS dev certificate — run
dotnet dev-certs https --clean && dotnet dev-certs https --trust to prevent Aspire Dashboard UntrustedRoot errors
Result: Complete project with Aspire AppHost (real PostgreSQL), ServiceDefaults, feature slices, FluentValidation, Result pattern, and proper EF Core predicate composition.
Example 2: Add a new slice
User says: "Add a CreateOrder slice under the Orders feature"
Actions:
- Load
templates/slice-template.md
- Load
references/data-access-guidance.md (mandatory — for EF Core performance rules and predicate composition)
- Generate request DTO, validator, handler, endpoint
- Place files in
Features/Orders/CreateOrder/
Result: Complete slice with FluentValidation, handler with Result return, HTTP mapping, and endpoint registration.
Example 3: Architecture review
User says: "Review this repo for anti-patterns"
Actions:
- Load
references/antipatterns.md
- Inspect current folder structure, dependency graph, and code patterns
- Identify smells and propose incremental fixes
Result: Prioritized list of findings with concrete, incremental migration steps — no "rewrite everything" proposals.
Example 4: Layered-to-slice migration
User says: "Refactor this layered Orders feature into vertical slices"
Actions:
- Load
references/architecture-principles.md
- Inspect existing Controllers, Services, Repositories for the feature
- Group by use case, create one slice per endpoint
- Move logic into slice handlers, remove empty abstractions
Result: Feature reorganized into Features/Orders/{CreateOrder,GetOrder,...}/ with thin endpoints and explicit dependencies.
Troubleshooting
Aspire AppHost won't start
Cause: Docker not running, or AppHost project SDK misconfigured.
Solution: Ensure Docker Desktop is running. The AppHost must use the dual SDK approach: Sdk="Microsoft.NET.Sdk" as the project SDK with <Sdk Name="Aspire.AppHost.Sdk" Version="9.2.1" /> as an additional SDK element. The .NET Aspire workload is deprecated in .NET 10 — use NuGet SDK packages instead. Ensure Properties/launchSettings.json exists with Aspire Dashboard OTLP endpoints configured. Run with --launch-profile https. See examples/aspire-apphost.md.
Rider shows "Unable to get project output" or empty Target Framework for AppHost
Cause: Rider requires the ".NET Aspire" plugin to handle Aspire AppHost projects.
Solution: Install the ".NET Aspire" plugin from Settings → Plugins → Marketplace. If already installed, try File → Invalidate Caches → Invalidate and Restart. As a fallback, run from the terminal: dotnet run --project src/Aspire/AppHost --launch-profile https.
PostgreSQL Running (Unhealthy) — password authentication failed
Cause: WithDataVolume persists PostgreSQL data (including the password set on first run), but Aspire generates a new random password on each restart. The volume retains the old password, causing authentication failures.
Solution: Use AddParameter("postgres-password", secret: true) and pass it to AddPostgres("postgres", password: postgresPassword). Store the password via dotnet user-secrets set "Parameters:postgres-password" "YourDevPassword123!". If the volume already has the wrong password, delete it: docker volume rm <volume-name> and restart. See examples/aspire-apphost.md.
Aspire Dashboard UntrustedRoot / SSL errors
Cause: The .NET HTTPS development certificate is missing or not trusted.
Solution: Run dotnet dev-certs https --clean && dotnet dev-certs https --trust. This is a one-time machine-level setup.
CS1061: AddServiceDefaults or MapDefaultEndpoints not found
Cause: Missing using ServiceDefaults; in Program.cs. These extension methods are in the ServiceDefaults namespace provided by the ServiceDefaults project.
Solution: Add using ServiceDefaults; to the top of Program.cs. Also ensure the API project has a <ProjectReference> to ServiceDefaults.csproj.
CS1061: UseNpgsql not found
Cause: Missing using Microsoft.EntityFrameworkCore; in Program.cs.
Solution: Add using Microsoft.EntityFrameworkCore; to the top of Program.cs. Ensure Npgsql.EntityFrameworkCore.PostgreSQL is referenced in the .csproj.
EF Core migration conflicts between slices
Cause: Multiple slices modifying the same DbContext independently.
Solution: Use a single shared DbContext per bounded context, not per slice. Slices share the DbContext but own their query/command logic. See references/data-access-guidance.md.
Slice handler grows too large
Cause: Business logic, validation, and data access mixed in one handler.
Solution: Extract domain services or value objects for complex logic. Keep the handler as orchestrator: validate -> call domain/service -> return Result. See references/architecture-principles.md.
FluentValidation not firing
Cause: Validator registered in DI but not explicitly called from the endpoint.
Solution: This skill uses explicit validation invocation in Minimal API endpoints, not middleware-based. Call validator.ValidateAsync(request) before the handler. See references/http-and-result-mapping.md.
Output expectations
When applying this skill in a real repo:
- explain the chosen slice/module placement
- state why EF Core or Dapper was selected
- state which interfaces are meaningful and why
- keep HTTP status mapping explicit
- avoid broad rewrites unless the user asked for them
- prefer incremental commits/patches in repository order
- preserve production-safe defaults