Skip to content
Back to skills

Dotnet Architecture

ASecurity

Decide the structure of an ASP.NET Core solution - project layout, feature slices vs layers, module seams, where business logic lives, data access shape, cross-cutting concerns (config, errors, observability, resilience, caching, background work, messaging), and when to split services. Use when starting a solution, adding a feature that doesn't fit the existing structure, introducing a new project or dependency, or reviewing architecture. For HTTP contracts use api-design; for shaping a modul...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentsgoc#expressazureapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add dmamulashvili/dotnet-agent-kit --skill dotnet-architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dotnet Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dotnet Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dmamulashvili-dotnet-architecture/badge)](https://www.skillsdirectory.com/skills/dmamulashvili-dotnet-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dotnet-architecture
description: Decide the structure of an ASP.NET Core solution - project layout, feature slices vs layers, module seams, where business logic lives, data access shape, cross-cutting concerns (config, errors, observability, resilience, caching, background work, messaging), and when to split services. Use when starting a solution, adding a feature that doesn't fit the existing structure, introducing a new project or dependency, or reviewing architecture. For HTTP contracts use api-design; for shaping a module's interface use codebase-design; for recording decisions use domain-modeling.
---

# .NET Architecture (ASP.NET Core, .NET 10)

Follow the structure already in the codebase when it exists, even if these defaults differ; propose a change as an ADR instead of mixing styles. Use these defaults for new code or when the codebase has no settled pattern. Every layer, project, package, and abstraction must answer a present need; "we might swap the database" is not one.

## Platform baseline

- Target .NET 10 (LTS, supported to November 2028). .NET 11 is STS (GA November 2026); move to it only for a feature you need now, such as C# 15 union types or closed class hierarchies, and record that as an ADR.
- Pin the SDK in `global.json` with `rollForward: latestFeature`.
- Install `dotnet-ef` as a local tool (`dotnet new tool-manifest`, then `dotnet tool install dotnet-ef`) at the same version as the EF Core packages, so everyone and CI run the same tools. The startup project needs `Microsoft.EntityFrameworkCore.Design`.
- Use the `.slnx` solution format (the .NET 10 SDK default for `dotnet new sln`).
- Shared build settings go in `Directory.Build.props` at the repo root: `Nullable` enabled, `ImplicitUsings` enabled, `TreatWarningsAsErrors` true, `AnalysisLevel` latest. Package versions go in `Directory.Packages.props` with Central Package Management (`ManagePackageVersionsCentrally`); no `Version` attributes in project files.

## Solution shape

Pick the smallest shape that fits, and grow along this path only when the trigger appears:

| Shape | Projects | Use when | Grow when |
|---|---|---|---|
| Single project, feature folders | `MyApp.Api`, `MyApp.Api.Tests` | One team, one deployable, domain still being discovered. The default. | Distinct business areas start reaching into each other's data or internals |
| Modular monolith | `MyApp.Api` host + one project per module (`MyApp.Orders`, `MyApp.Billing`) + tests | Several business areas with real seams between them, still one deployable | A module needs independent deploys, scaling, or ownership by a separate team |
| Separate services | One deployable per service | Independent deploy/scale/team needs that the monolith blocks today | - |

- Don't start with microservices. Splitting is cheap from a modular monolith with clean seams and expensive from a tangled one.
- Clean Architecture's Domain/Application/Infrastructure/Api project split is not the default. Use it only when the domain model is large and invariant-heavy enough that compile-time isolation from infrastructure pays for the extra indirection; feature folders inside a module give most of the benefit.

## Feature slices

Organize by feature, not by technical type. A feature folder holds everything for one use case:

```
src/MyApp.Api/
  Program.cs
  Features/
    Orders/
      CreateOrder.cs        // request, response, validation, handler, endpoint mapping
      GetOrder.cs
      OrderEndpoints.cs     // MapGroup("/orders") and registration of the slice endpoints
      Order.cs              // entity, when only this feature area owns it
  Infrastructure/
    AppDbContext.cs
    Configurations/         // IEntityTypeConfiguration<T> per entity
  Common/                   // only code used by 3+ features
```

- Use minimal APIs with `MapGroup` per feature area and `TypedResults`. In a codebase already built on controllers, keep controllers rather than mixing styles.
- A slice may duplicate a little code from another slice; extract only on the third use, and only if the copies change for the same reason.
- Endpoints stay thin: bind, call the handler or service, map the result to a typed result. Business rules live in the handler, a service, or the entity.
- Handlers are plain classes registered in DI and injected into the endpoint. No mediator library by default: MediatR and MassTransit went commercial in 2025, and a mediator adds indirection without a present need. If pipeline behaviors (cross-cutting per handler) become a real need, write a small decorator or use an endpoint filter first.
- Map DTOs by hand with static methods or extension methods next to the slice. If mapping volume becomes a burden, use a source generator (Riok.Mapperly) rather than reflection-based mappers.

## Modules

When the solution becomes a modular monolith:

- Each module owns its tables, preferably in its own schema, and its own `DbContext`. No module queries another module's tables.
- Modules talk through a small public contract (interfaces and DTOs in the module project, or a `MyApp.Orders.Contracts` project), or through in-process events. Everything else is `internal`.
- Each module exposes one registration method (`services.AddOrdersModule(config)`, `app.MapOrdersEndpoints()`) called from the host.
- Enforce the seams with architecture tests (`TngTech.ArchUnitNET`) so they don't erode.

## Domain logic

- Put invariants on the entity: private setters, a constructor or factory that only builds valid instances, methods named for business operations (`order.Cancel(reason)`), not public property bags edited from outside.
- Use value objects (records, or EF Core complex types) for concepts with rules: `Money`, `EmailAddress`, `DateRange`.
- Keep simple CRUD simple: an entity with no invariants doesn't need a rich model.
- Expected failures (validation, not found, business rule violations) are return values that become ProblemDetails, not exceptions. Use a small result type or C# 15 unions once on .NET 11. Throw only for unexpected failures, and handle those centrally with `IExceptionHandler` and `AddProblemDetails()`.

## Data access

- `DbContext` is the unit of work and the repository. Inject it directly into handlers; don't wrap it in a generic repository.
- Add a specific repository only when the same non-trivial query or aggregate load is reused across several slices, or to hide a module's persistence from other modules.
- Entity configuration lives in `IEntityTypeConfiguration<T>` classes, applied with `ApplyConfigurationsFromAssembly`.
- Reads project to DTOs with `Select` and `AsNoTracking`; writes load the entity, call its methods, and `SaveChangesAsync` once per request.
- Bulk changes that need no domain logic use `ExecuteUpdateAsync` / `ExecuteDeleteAsync` instead of loading entities.
- Map value objects and nested data as complex types (`ComplexProperty`, with `.ToJson()` for a JSON column), not owned entities.
- Soft delete and multi-tenancy use named query filters (`HasQueryFilter("SoftDelete", ...)`), so a query can turn off one filter with `IgnoreQueryFilters(["SoftDelete"])` and keep the rest.
- Use optimistic concurrency tokens on entities edited concurrently. For slow or incorrect EF Core queries, use the `optimizing-ef-core-queries` skill from the `dotnet-data` plugin; for migrations, follow the `ef-migrations` rule.

## Cross-cutting concerns

| Concern | Default |
|---|---|
| Configuration | Options pattern: `AddOptions<T>().BindConfiguration("Section").ValidateDataAnnotations().ValidateOnStart()`. Secrets from user-secrets locally and a vault (e.g. Azure Key Vault) in deployed environments; never in `appsettings.json`. |
| Validation | Built-in minimal API validation: `builder.Services.AddValidation()` with DataAnnotations or `IValidatableObject`. Add FluentValidation only for rules DataAnnotations can't express cleanly. |
| Errors | `AddProblemDetails()`, `UseExceptionHandler()`, and an `IExceptionHandler` for unexpected exceptions. Log inside the handler: since .NET 10, exceptions it handles are no longer logged by default. Shapes per the `api-design` skill. |
| Observability | OpenTelemetry for traces, metrics, and logs (`AddOpenTelemetry().WithTracing(...).WithMetrics(...)`, exported via OTLP). Structured logging through `ILogger` with message templates; source-generated `[LoggerMessage]` on hot paths. Health checks: readiness (dependencies included) at `/health`, liveness (process only) at `/alive`. |
| Outbound HTTP | Typed clients via `AddHttpClient<T>()` plus `AddStandardResilienceHandler()` from `Microsoft.Extensions.Http.Resilience`. Never `new HttpClient()` per call. |
| Caching | `HybridCache` (`AddHybridCache()`) for in-process plus distributed caching with stampede protection. Output caching for whole GET responses. |
| Time and randomness | Inject `TimeProvider`; never call `DateTime.UtcNow` in business logic. |
| Background work | `BackgroundService` for in-process periodic or queue work, with a scoped `DbContext` created per iteration via `IServiceScopeFactory`. A scheduler (Quartz.NET, Hangfire) only for persisted schedules or retries across restarts. |
| Messaging | In-process events first. For a broker, use the official client (`Azure.Messaging.ServiceBus`, `RabbitMQ.Client`) or an MIT library (Wolverine, Rebus); MassTransit v9 needs a commercial license. Publish integration events through a transactional outbox so a message is never sent for a rolled-back change. |
| Auth | ASP.NET Core authentication and authorization policies; authorize at the endpoint group (`RequireAuthorization("policy")`), and check resource ownership in the handler. |

## Dependency injection

- Register by feature or module through extension methods, not one giant `Program.cs`.
- Constructor injection with primary constructors. Their parameters are mutable captures, not `readonly` fields: never reassign one, and assign it to a `private readonly` field when the class is large or the value must not change. No service locator (`IServiceProvider.GetService` in business code).
- Lifetimes: `DbContext` and anything holding it scoped; stateless services scoped or singleton; never inject a scoped service into a singleton.
- Interfaces only where there is more than one implementation or a test needs to replace something you don't own; a concrete class with one implementation needs no interface.

## Before finishing an architecture change

- [ ] It follows the existing structure, or the deviation is recorded as an ADR candidate
- [ ] Every new project, package, or abstraction is tied to a present need
- [ ] Module seams hold: no cross-module table access, internal by default
- [ ] Business rules live in handlers, services, or entities, not endpoints
- [ ] Configuration is validated on start; secrets are not in source
- [ ] New outbound calls have resilience; new background work creates its own scope
- [ ] Hard-to-reverse choices (solution shape, module split, messaging, database) are listed for the `domain-modeling` skill to record

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.