Backend
The .NET conventions, the reasoning, and the patterns for everyday work.
Backend apps are .NET and C#. The source of truth is the Plain engineering skills. This page explains how the pieces fit and when to reach for each one; the skills hold the exhaustive rules.
Two skills anchor everything. plain-engineering-conventions is language-agnostic (errors, boundaries, observability, testing, security). plain-dotnet-guardrails is the .NET side (VSA, EF Core, Aspire, DI, build hygiene, architecture tests). Read those two first.
Local orchestration with Aspire
Every Platform .NET app runs its whole local system through .NET Aspire: the AppHost describes the composition in C# instead of docker-compose YAML, and the same dotnet run --project src/{Project}.AppHost (wired as pnpm dev) starts the database, the API, and the web frontend in dependency order with health gating. Three pieces:
- AppHost — an executable project that declares resources and services. Containers get persistent lifetimes and data volumes; services get injected connection strings and
WaitForhealth gating; the frontend joins through the CommunityToolkit NodeJS extension with a fixed dev port. It is the only composition: repos drop their docker-compose files rather than carrying two definitions in parallel. - ServiceDefaults — a shared project every service references.
AddServiceDefaults()brings OpenTelemetry (logs, traces, metrics), service discovery, default resilience on everyHttpClient, and/health+/aliveendpoints. The Aspire dashboard shows the whole system's logs, traces, and resource health locally, with zero configuration. - Client integrations — packages such as
Aspire.Microsoft.EntityFrameworkCore.SqlServerwire pooling, health checks, and SQL spans into EF Core from the connection name the AppHost injects. Connection strings are resolved by name at runtime and never hardcoded inappsettings.json.
The shape in practice:
var builder = DistributedApplication.CreateBuilder(args);
var sql = builder.AddSqlServer("sql")
.WithLifetime(ContainerLifetime.Persistent)
.WithDataVolume();
var db = sql.AddDatabase("appdb");
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(db).WaitFor(db)
.WithExternalHttpEndpoints();
builder.AddPnpmApp("web", "../../apps/web", "dev")
.WithPnpmPackageInstallation()
.WithHttpEndpoint(port: 3000, env: "PORT")
.WithEnvironment("NEXT_PUBLIC_API_URL", api.GetEndpoint("http"))
.WithExternalHttpEndpoints()
.WaitFor(api);Guardrails that every AppHost carries: .WithDataVolume() on stateful containers (data survives volume recreation, not just restarts), .WithRedisCommander() when Redis is in the graph, and .WaitFor(...) on every resource reference so services start only when dependencies are healthy. Resource names, connection string patterns, and the project-class naming pitfall are in the aspire-conventions reference of plain-dotnet-guardrails.
Architecture
Apps use Clean Architecture layers with the dependency arrow pointing inward:
Api minimal-API endpoints, grouped by feature; the composition root
Application use-cases as commands and queries (CQS); DTOs
Domain entities, value objects, domain events; no outward dependencies
Infrastructure EF Core and external integrations; implements Application/Domain interfaces
SharedKernel DDD building blocks reused across the domainA request flows one direction. It lands on an endpoint in Api, which dispatches a command or query to Application. The handler works with the Domain model and persists through Infrastructure. Domain knows nothing about the outer layers, which is what keeps the core testable and stable. The full reference is in Architecture; deeper tactical patterns are in the clean-ddd-hexagonal skill.
Vertical slices and CQS
Inside the layers, organize by feature, not by technical type. Everything for "create invoice" lives together rather than being scattered across generic Services and Repositories folders. Separate the write side from the read side: commands change state and return a result, queries read and never mutate. This keeps handlers small and intention-revealing.
Errors as values
Domain and application errors are return values, not thrown exceptions, using ErrorOr<T> mapped to RFC 7807 ProblemDetails at the edge:
public async Task<ErrorOr<User>> Handle(GetUser query)
{
var user = await _users.FindAsync(query.Id);
return user is null
? Error.NotFound("User.NotFound", "User not found")
: user;
}
// endpoint maps the result to HTTP
result.Match(Results.Ok, errors => errors.ToProblemDetails());Exceptions are for the genuinely exceptional, not for control flow.
When to use what
- A service or small app: a few layered projects with feature folders inside. This is the default. Do not add modules or a mediator before you feel the pain.
- A larger system: a modular monolith where each module is a bounded context, internally VSA and CQS. Split into separate deployables only when a boundary genuinely demands it.
Data access
EF Core is the default. Keep queries in Infrastructure behind interfaces the Application layer owns. Project to DTOs for reads instead of loading full aggregates, use AsNoTracking for read-only queries, and watch for N+1 with explicit Include or split queries. Repository and query patterns are in dotnet-backend-patterns; tuning is in optimizing-ef-core-queries.
Testing
Follow the testing trophy, not the pyramid (Kent C. Dodds; Spotify's honeycomb is the same shape for services with many collaborators). Most of the weight goes to integration tests: run a real host through WebApplicationFactory against a real database through Testcontainers and assert behaviour through the HTTP surface, so you catch the wiring, mapping and query bugs that mocks hide. Add unit tests with xUnit only where there is logic with real branching, such as domain rules, calculations, parsers and state machines, where the value-as-errors style makes assertions clean. Keep end-to-end tests few, over the critical journeys. Static analysis (the compiler, analyzers and check-diff) is the base layer. A bug fix gets one regression test. Do not test constants, configured defaults, mocks of your own code or markup details: those tests fail when someone edits the code, not when behaviour breaks. Skill: csharp-xunit.
Azure SQL backups in production
Every production environment manages its database backup policy from Bicep, explicitly, with the platform standard values. Restating the defaults is the point: an unmanaged policy is one portal click or one implicit reset away from drifting, and a backupShortTermRetentionPolicies deployment that only sets retentionDays silently resets the differential cadence to Azure's 12-hour default on every apply.
The standard is a 7-day point-in-time restore window, one differential backup every 24 hours, and no long-term retention unless the domain demands it:
resource backupRetention 'Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies@2023-08-01-preview' = {
parent: database
name: 'default'
properties: {
retentionDays: 7 // PITR window, the platform production standard
diffBackupIntervalInHours: 24 // stated so an apply cannot reset it to 12
}
}Wire both values from the environment config (infra/pro.env) through the infra workflow, so pro carries them and PRE stays on the platform defaults. Verify after the first apply in the portal under SQL server, Backups, Retention policies.
Observability and security
Structured logging with ILogger<T> and OpenTelemetry for traces and metrics. Never log secrets or personal data. Run dependency and secret scanning in CI. The plain-engineering-conventions references cover observability and security in depth.
Keeping apps out of search engines
Platform apps are internal, on *.plainconcepts.com subdomains, and must not show up next to the company's public site:
- Send
X-Robots-Tag: noindex, nofollowon every response, HTML or not, from a middleware registered first. - Serve
/robots.txtwithDisallow: /behind theRobots:DisallowAllsetting, and switch it on only after thenoindexheader has been live: if crawling is blocked first, the crawler never sees thenoindexand an indexed URL can stay indexed. New hostnames can switch it on from day one. - The root goes straight to the Entra ID sign-in for an anonymous visitor and shows none of the app's own content.
Skills for agents
plain-engineering-conventions, plain-dotnet-guardrails, clean-ddd-hexagonal, dotnet-backend-patterns, modern-csharp-coding-standards, optimizing-ef-core-queries, csharp-xunit. See AI and agents for how to install them.
Frontend
The frontend stack, the reasoning behind it, and the patterns for everyday work.
AI x SDLC
AI across the Platform delivery lifecycle on one page: GitHub Agentic Workflows on the agents-arc runner fleet against the Forge gateway, agent-harness installing the Platform Harness, the merge gate and merge train, Health measuring the result, and the full workflow contract.