This repository is a Clean Architecture API starter template named CleanApiStarter.
- Use
CleanApiStarterfor solution, project, assembly, and namespace naming. - Do not introduce
CleanArchitecturenamespaces or assembly names. - Keep project names fully qualified:
CleanApiStarter.ApiCleanApiStarter.ApplicationCleanApiStarter.ConfigurationCleanApiStarter.DomainCleanApiStarter.InfrastructureCleanApiStarter.AppHostCleanApiStarter.AspNetCoreCleanApiStarter.UnitTests
- Keep clean architecture application layers under the
/src/solution folder:- API
- Application
- Configuration
- Domain
- Infrastructure
- Keep Aspire/runtime support projects under the
/src/Common/solution folder inCleanApiStarter.slnx:- AppHost
- AspNetCore
- Keep database scripts under top-level
database/migrations. - Keep important root files in solution items, including
README.md,docker-compose.yml,Directory.Build.props,Directory.Packages.props,.editorconfig,.gitignore, andglobal.json.
- Dependencies point inward:
Domainreferences nothing.ApplicationreferencesDomain.Configurationreferences no application layers and contains only plain options classes plus options registration helpers.InfrastructurereferencesApplicationandConfiguration.ApicomposesApplication,Infrastructure,Configuration, andAspNetCore.
- Keep repository interfaces in
Application, notDomain. - Keep database implementation details in
Infrastructure. - Keep domain models persistence-agnostic. Persistence mapping details belong in Infrastructure EF Core configuration.
- Use
requiredfor non-null required scalar properties on DTOs and domain entities. Do not use= string.Emptyonly to satisfy nullable reference type warnings. - Keep collection properties initialized with
= []. - Keep EF navigation properties as
= null!when EF is responsible for materializing them. - Use nullable types such as
string?orDateTime?only for genuinely optional values. - Use EF Core through
ApplicationDbContextfor application persistence. Do not reintroduce Dapper for the default template data access path. - Keep
ApplicationDbContextinCleanApiStarter.Infrastructure/Persistence, not inIdentity. The context owns both application entities and Identity storage, so it is a persistence concern. - Keep EF Core fluent API entity maps in
CleanApiStarter.Infrastructure/Persistence/ConfigurationusingIEntityTypeConfiguration<T>. Do not put entity mapping logic directly insideApplicationDbContextunless there is a very small one-off reason. - Keep Identity-specific classes, such as
ApplicationUserand auth services, underCleanApiStarter.Infrastructure/Identity.
- Organize the
Applicationproject by feature. Put feature-specific contracts, DTOs, and application services under folders such asApplication/Features/ProjectsorApplication/Features/Auth. - Do not create generic
Application/ServicesorApplication/Modelsfolders for feature-specific code. - Keep cross-cutting application abstractions under
Application/Common, for exampleApplication/Common/Interfaces. - Return single resources as their DTO object directly.
- Use
ArrayResult<T>instead of returning raw arrays from non-paginated list endpoints. - Use
PaginatedQueryfor paginated request parameters and returnPaginatedResult<T>directly for paginated collection responses. - Use FluentValidation for request validation. Put validators beside feature request models in
Application, and rely on the shared Minimal API validation endpoint filter inCleanApiStarter.AspNetCore. FluentValidation failures should return422 Unprocessable Entity. - Do not use DataAnnotations for application request validation.
- Use Scalar, not Swagger/Swashbuckle.
- Use the .NET 10 API versioning/OpenAPI setup from the Microsoft .NET blog:
Asp.Versioning.Httpv10Asp.Versioning.Mvc.ApiExplorerv10Asp.Versioning.OpenApibuilder.Services.AddApiVersioning(...).AddApiExplorer(...).AddOpenApi();app.MapOpenApi().WithDocumentPerVersion();app.MapScalarApiReference(...)configured fromapp.DescribeApiVersions()
- Do not add
Swashbuckle.AspNetCore. - Cancellation tokens are explicit:
- Do not use
CancellationToken cancellationToken = defaultin service or repository contracts. - API actions should accept
CancellationToken cancellationTokenand pass it through.
- Do not use
- Use explicit local types. The
.editorconfigprefers explicit types overvar. - Use project-level
GlobalUsings.cs; avoid adding file-levelusingdirectives unless there is a very specific reason.
- Use a single Postgres database:
postgres. - Do not create or reference a separate feature-specific database.
- AppHost should expose the API connection from the
postgresresource, and application settings should read it throughConnectionStrings:Postgres. docker-compose.ymlshould usePOSTGRES_DB=postgres.- Database schema scripts live in
database/migrations, for example:database/migrations/V001__create_projects_and_tasks_tables.sql
- Do not add API startup database initialization such as
DbInitializeror DbUp calls. Aspire/Docker init scripts own local schema creation. - EF Core is used for application data access and Identity storage, but schema creation still belongs to the SQL scripts in
database/migrations. - Docker Postgres init scripts run only on first volume creation. If scripts need to replay, delete the old volume.
- This repo uses
postgres:latest. Because Postgres 18+ expects the data volume mounted at/var/lib/postgresql, do not mount the volume at/var/lib/postgresql/data. - In Aspire, use a server resource name that does not conflict with the database resource name, for example:
- server resource:
postgres-server - database resource:
postgres
- server resource:
- Aspire Postgres should use a volume mounted at
/var/lib/postgresql.
- Keep
CleanApiStarter.AspNetCore. - It centralizes Aspire-friendly runtime defaults:
- OpenTelemetry traces, metrics, logs, and OTLP export
- problem-details exception handling
/versionendpoint- health endpoints
- service discovery
- default HTTP client resilience
- security defaults such as removing the Kestrel
Serverresponse header
- API
Program.csshould stay small and call:builder.AddAspNetCoreDefaults();app.UseAspNetCoreDefaults();app.MapDefaultEndpoints();
- Shared middleware such as HTTP request logging belongs in
CleanApiStarter.AspNetCore, not duplicated inside each API project. - Keep OpenTelemetry logs configured to include scopes, formatted messages, and parsed state values so structured message-template properties show up in Aspire.
- Responses should include
X-Request-IDwith the current trace id, configured centrally inCleanApiStarter.AspNetCore. - Response compression should be configured centrally in
CleanApiStarter.AspNetCorewith Brotli and gzip providers. - Use structured logging message templates instead of interpolated log strings. Prefer stable property names like
{ProjectId},{TaskId}, and{UserId}. - Register root settings once with
AddAppSettings(builder.Configuration), then injectAppSettingsdirectly when services need configuration values. - Do not add generic options registration helpers until the template has multiple real options sections that need them.
- Do not create a broad
Sharedproject. Keep cross-project settings inCleanApiStarter.Configuration. - Keep dependency injection validation enabled with
ValidateOnBuildandValidateScopes.
- Prefer Minimal APIs for this template.
- Keep
Program.cssmall by placing route groups in endpoint group classes under version folders such asApi/Endpoints/V1/Projects.cs. - Endpoint group classes should implement
IEndpointGroupfromCleanApiStarter.AspNetCoreand be mapped throughapp.MapEndpoints(Assembly.GetExecutingAssembly());. - Use built-in Minimal API mapping methods with explicit
.WithName(...); do not add customMapGet/MapPostoverloads that shadow framework methods. - API versions are selected with the optional
X-Api-Versionrequest header. Missing versions default to v1. - Endpoint groups should declare
MajorVersionto match their folder, for exampleV1uses1andV2uses2. - Endpoint names must be globally unique across versions. Prefer names suffixed with the version, such as
GetProjectsV1andGetProjectsV2. - Do not reintroduce MVC controllers unless the template intentionally changes direction.
- Authentication is API-first:
- clients obtain a Google ID token
POST /api/auth/googlevalidates it- the API issues its own JWT
- Keep JWT bearer authentication setup in
CleanApiStarter.AspNetCore. - Keep local user/role storage in ASP.NET Core Identity under
CleanApiStarter.Infrastructure. - Do not use cookies as the default API auth mechanism.
- Keep the development Google login helper page unversioned so it can be opened directly in a browser.
- Protected API calls should send
Authorization: Bearer <api-jwt>. SendX-Api-Versiononly when selecting a non-default API version.
- Manage versions centrally in
Directory.Packages.props. - Keep
PackageVersionitems sorted alphabetically byInclude. - Do not add package versions directly in individual
.csprojfiles.
- This repository is also the
dotnet newtemplate source. - Keep template metadata in
.template.config/template.json. - Keep NuGet template package metadata in
CleanApiStarter.Template.csproj. - Use
dotnet packanddotnet nuget pushfor template packaging and publishing. Do not usenuget pack,nuget.exe, or Mono. - Use
scripts/install-template.shto pack and install the local template. - Keep repo-only template packaging scripts excluded from generated template output.
- Keep CodeQL security scanning in
.github/workflows/codeql.yml, and allow generated projects to inherit it. - Keep release publishing triggered by GitHub Release publication with
vX.Y.Ztags, not manual version inputs.
- Use xUnit v3, AutoFixture.xUnit3, AutoFixture.AutoNSubstitute, NSubstitute, and Shouldly for unit tests.
- Reusable test helpers belong in
CleanApiStarter.Tests. - Application unit tests should reference
CleanApiStarter.Testsinstead of duplicating common test setup. - API integration tests use MSTest and Testcontainers for Postgres.
- API integration tests should keep real JWT bearer authentication active. Generate test JWTs from
appsettings.Testing.jsoninstead of replacing authentication with a fake scheme. - API integration tests should start a Postgres Testcontainer and apply SQL scripts from
database/migrations. - Keep test app settings in
appsettings.Testing.json. - Test methods must follow the naming pattern
UnitOfWork_StateUnderTest_ExpectedBehavior. - Tests must follow AAA format with explicit
// Arrange,// Act, and// Assertsections.
- After structural or package changes, run:
dotnet restore CleanApiStarter.slnx
dotnet build CleanApiStarter.slnx --no-restore /nr:false -v:minimal- Aspire AppHost builds may need to run outside a sandbox because the Aspire SDK touches local runtime/process resources.