Org Skills

dotnet-architecture

Architect-level guidance for structuring .NET back-end systems — modular monolith first, vertical slices vs clean/onion layering, project and assembly boundaries, pragmatic domain modelling, CQRS and MediatR trade-offs, messaging with the outbox pattern and idempotency, data ownership, API versioning, cross-cutting concerns, per-environment configuration, OpenTelemetry, local orchestration and container deployment, captured as ADRs.

Download .zip Raw Source
When agents use itUse this whenever the user is starting a .NET solution, laying out a *.sln or *.slnx with multiple *.csproj projects, choosing between layers and feature folders, debating microservices, adding a message broker, designing aggregates, a shared DbContext or a Directory.Build.props, or asks "how should I structure this .NET service". Also apply it when reviewing a .NET solution or pull request for architectural smells, even if the user only asks about one class.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill dotnet-architecture -a github-copilot
# or with the org installer (adds .github/skills/dotnet-architecture):
npx -y github:AGCO-Global/org-skills add skill dotnet-architecture

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill dotnet-architecture
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill dotnet-architecture -g

Installs the backend-dotnet-skills plugin, which bundles all Backend / .NET skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-dotnet-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill dotnet-architecture -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill dotnet-architecture -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill dotnet-architecture -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill dotnet-architecture -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

.NET Architecture

Think like the engineer who will still own this system in three years: optimise for changeability and clear boundaries, not for the number of patterns applied. Explain each trade-off to the user rather than dictating, and target the current LTS release of .NET unless the project pins otherwise.

1. Decide the deployment shape first

Question Default Change it when
One deployable or many? Modular monolith: one host, modules with enforced boundaries Independent scaling/release cadence is proven necessary, teams are blocked on each other, or regulatory isolation is required
Microservices Only with mature CI/CD, observability, on-call and a platform team Never as the starting point for a new product with one team
Sync vs async between modules In-process calls through a module's public contract Work is slow, cross-module side effects must not block the request, or modules will be extracted later → messages
Database One server, schema (or at least table ownership) per module A module is extracted — its schema moves with it

A well-bounded modular monolith is the cheapest path to microservices if you ever need them; a distributed monolith is the most expensive path to anything.

2. Code organisation: vertical slices vs layers

Style Fits Costs
Vertical slices (feature folders: Orders/PlaceOrder/{Endpoint, Handler, Validator, Tests}) CRUD-heavy or workflow APIs, most line-of-business services Shared logic must be extracted deliberately; needs discipline to avoid copy-paste
Clean / onion (DomainApplicationInfrastructure, Api) Rich domain rules, long-lived core, multiple delivery mechanisms More projects and mapping; ceremony if the domain is thin
Hybrid (slices inside each module, shared Domain per module) Modular monolith with some complex modules Needs clear written conventions

Default: modules as top-level boundaries, vertical slices inside, a real domain layer only in modules whose rules justify it.

3. Project and assembly boundaries

src/
  Host/                     ← composition root: Program.cs, config, auth, OTel
  Modules/Orders/
    Orders.Contracts/       ← public DTOs, integration events, module interface
    Orders/                 ← internal implementation (slices, domain, EF DbContext)
  Modules/Billing/...
  BuildingBlocks/           ← small, stable: result types, outbox, messaging abstractions
tests/
Directory.Build.props       ← nullable, warnings-as-errors, analyzers, LangVersion
Directory.Packages.props    ← central package management
  • Modules reference each other's Contracts only. Make implementation types internal; enforce with architecture tests.
  • A project boundary is a compile-time boundary — use it where it enforces something, not for every folder. Ten projects for a 20-endpoint service is ceremony.
  • BuildingBlocks must not become a dumping ground; if it depends on a module, it's in the wrong place.

4. Domain modelling without ceremony

  • Aggregates own invariants and are the unit of consistency: one transaction changes one aggregate; cross-aggregate rules are eventually consistent via domain/integration events.
  • Value objects as record/readonly record struct (Money, EmailAddress) with validation in the factory — kills primitive obsession.
  • Private setters and behaviour methods (order.Cancel(reason)) rather than public setters driven by services; EF Core maps private fields/backing fields fine.
  • Reference other aggregates by id, not navigation properties across module boundaries.
  • Skip DDD tactical patterns in CRUD modules — an anaemic model is fine when there are no invariants.

5. CQRS and MediatR

Need Recommendation
Separate read and write models Yes: writes go through aggregates; reads are AsNoTracking projections (or Dapper/SQL views) straight to DTOs
In-process mediator Optional. Helps with uniform pipeline behaviours (validation, logging, transactions) across many handlers
Avoid when Handlers are one-liners, the team finds navigation harder, or endpoint filters/decorators already cover the cross-cutting needs

A plain handler class injected into the endpoint is often clearer than ISender.Send. Check the current licence terms of MediatR, MassTransit, AutoMapper and similar libraries before adopting — several moved to commercial licensing for newer major versions.

6. Messaging and consistency

  • Broker abstraction: MassTransit or NServiceBus (sagas, retries, outbox built in) over raw broker SDKs (Azure Service Bus, RabbitMQ, Kafka) unless you need low-level control.
  • Transactional outbox whenever a state change and a published message must both happen: write the message to an outbox table in the same DB transaction, relay it afterwards. Never "save, then publish" and hope.
  • Idempotent consumers: delivery is at-least-once. Deduplicate by message id (inbox table) or make the operation naturally idempotent.
  • Distinguish domain events (in-process, same transaction) from integration events (versioned contracts in *.Contracts, published via outbox).
  • Retries with backoff, then dead-letter with alerting; poison messages must not block the queue.

7. Data ownership

  • Each module owns its tables and its DbContext (use HasDefaultSchema("orders")); no cross-module joins or foreign keys. Need another module's data → call its contract, or keep a local read-model fed by events.
  • Reporting that must join everything goes to a separate read store/warehouse, not into the modules.

8. API design and versioning

  • Version from day one with Asp.Versioning.Http (minimal APIs) or Asp.Versioning.Mvc — URL segment (/v1/) is the most operable default.
  • Additive changes don't need a new version; removing/renaming fields does. Diff the OpenAPI document in CI to catch breaking changes.
  • Idempotency keys on POSTs that create money-moving or external side effects.

9. Cross-cutting concerns

Concern Where it lives
AuthN/AuthZ Host: authentication + named policies; modules declare required policies on their endpoint groups
Validation, logging, transactions Endpoint filters, middleware, or mediator pipeline behaviours — one mechanism per solution
Errors ProblemDetails + IExceptionHandler in the host
Rate limiting, CORS, output caching Built-in ASP.NET Core middleware configured in the host
Time, ids, current user Injected abstractions (TimeProvider, ICurrentUser) for testability

10. Configuration per environment

  • Same artefact in every environment; differences come from environment variables, appsettings.{Environment}.json (non-secret) and a secret store provider. Validate options at startup (ValidateOnStart).
  • Feature flags (e.g. Microsoft.FeatureManagement) instead of environment-name ifs in business code.

11. Observability

  • OpenTelemetry for traces, metrics and logs (AddOpenTelemetry().WithTracing(...).WithMetrics(...)), exporting via OTLP; ASP.NET Core, HttpClient and EF Core instrumentation plus custom ActivitySource/Meter for business operations.
  • Propagate trace context across messages (MassTransit/NServiceBus do this). Every log line correlates to a trace id.

12. Local orchestration and deployment

  • Aspire (formerly .NET Aspire) can orchestrate the host, databases and brokers locally with OTel dashboards and service defaults. Treat it as a developer-experience tool; evaluate its deployment tooling separately against your platform.
  • Containers: dotnet publish container support or a multi-stage Dockerfile, non-root user, chiseled/distroless base images where possible.
  • Health checks: /health/live (process up, no dependencies) vs /health/ready (dependencies reachable) via AddHealthChecks(); wire to orchestrator probes.
  • Graceful shutdown: honour SIGTERM, drain in-flight requests and consumers within the host shutdown timeout.

Deliverable format

For architecture requests, produce:

  1. Context and forces — load, team size, domain complexity, compliance constraints (ask if unknown).
  2. Recommendation — deployment shape, module map, code style, with a solution tree.
  3. ADR(s) in the form: Title / Status / Context / Decision / Consequences (positive and negative) / Alternatives considered.
  4. Migration path from the current state in small, independently shippable steps.
  5. Guardrails — architecture tests, analyzers and CI checks that keep the decision enforced.

Anti-patterns to reject

  • Microservices for a single team's greenfield product; shared database across "services".
  • A Common/Shared/Utils project every module depends on and everyone edits.
  • Generic repository over EF Core that hides IQueryable power and adds nothing (IRepository<T> with GetAll()).
  • Publishing messages outside the DB transaction without an outbox; non-idempotent consumers.
  • Cross-module joins, foreign keys or navigation properties; one giant DbContext for everything.
  • MediatR (or any pattern) adopted by default, then used to call handlers from handlers.
  • Environment-specific builds, secrets in config files, business logic branching on environment names.
  • Mapping layers that copy identical shapes three times "for purity".