This template can be used to bootstrap a working full-fledged ASP.NET Web Api project with a single CLI command (see below).
It contains what I consider to be best practices/patterns, such as CQRS, Mediator, Clean Architecture.
If you like this project, you learned something from it or you are using it in your applications, please press the star button. Thanks!
I found implementations of similar samples/templates to often be overly complicated and over-engineered (IMO). This is an effort to create a more approachable, more maintainable solution that can be used as a starting point for the majority of real-world projects while, at the same time, striving to reach a sensible balance between flexibility and complexity.
- Based on .NET 10 to have access to the latest features
- Minimal hosting model (top-level statements in
Program.cs) - CQRS: commands and queries have separate handlers and decorator pipelines (dispatched through a mediator), and the data access is segregated too — read repositories for queries, write repositories behind the unit of work for commands
- Simple Mediator abstraction for CQRS and implementation relying on the chosen Dependency Injection container (see HumbleMediator)
- Project structure following Clean Architecture principles
- Read/write repository split (the data-access half of CQRS) over Entity Framework Core, behind tech-agnostic interfaces —
Application/Corenever reference EF, so a repository can be backed by Dapper or raw ADO.NET without touching business logic:- Reads (
IReadRepository<T>) are injected directly into query handlers and run on a fresh no-tracking context — no transaction, and a natural seam to later target a read replica - Writes (
IWriteRepository<T, TKey>) are resolved inside the unit of work: the delegate passed toIUnitOfWorkFactory.ExecuteInTransactionAsyncis the atomic, retriable transaction —uow.GetRepository<T>()stages changes while the unit of work flushes and commits on success (and rolls back on exception)
- Reads (
- Automatic repository registration: read repositories (
ReadRepositoryBase<>) and write repositories (WriteRepositoryBase<,,>) are discovered in theInfrastructureassembly and registered by a singleAddPersistence<TDbContext>()call (which also wires the unit-of-work factory for the write side) - Read repositories ship built-in pagination, sorting, and free-text search scaffolding (
PagedResult<T>,ListAsync) - PostgreSQL open source database as data store (easily replaceable with any Entity Framework-supported data stores)
- Connection resiliency: transient database failures are retried automatically (
EnableRetryOnFailure); on a retry the unit-of-work delegate is replayed on a freshDbContext, which is why it must stay idempotent (no non-database side effects inside the transaction). Replay makes writes at-least-once (a lost commit acknowledgement can replay an already-committed transaction); for exactly-once, pass averifySucceededcallback toExecuteInTransactionAsync— write a unique token row inside the transaction and reportIsCommittedby checking for that row, so the strategy can confirm the prior commit instead of repeating it. Run that verification query on a retry-enabled context (e.g. one from the sameIDbContextFactory, which already hasEnableRetryOnFailure): the connection that failed during commit is likely to fail again during verification. This is EF Core's Option 4 — Manually track the transaction - Database configured to use snake_case naming convention via EFCore.NamingConventions
- Migrations handled by Entity Framework and automatically applied during startup (in dev environment)
- SimpleInjector open-source DI container integration for advanced service registration scenarios
- Aspect-oriented programming using Decorators on the above-mentioned mediator
- Logging: QueryHandlerLoggingDecorator and CommandHandlerLoggingDecorator
- Validation: CommandHandlerValidationDecorator and QueryHandlerValidationDecorator
- Caching is intentionally not a mediator decorator — it's a per-resource policy (key scope, TTL, invalidation), not a uniform cross-cutting concern like logging or validation. Reach for purpose-built layers instead: output caching for HTTP responses, and
HybridCacheused deliberately per-handler (with explicit, scoped keys and invalidation) for application-level caching - Structured logging using the standard MEL interface with the open-source Serilog logging library implementation
- Cache-friendly Dockerfile with a
/healthcontainer HEALTHCHECK - Expressive testing using xUnit and AwesomeAssertions
- Integration testing using real database implementation with Testcontainers
- Central Package Management
- Continuous integration via GitHub Actions: build, dependency vulnerability scan with grype, and Docker image build
- pre-commit hooks: C# formatting with CSharpier, secret scanning with gitleaks, and general file-hygiene checks
Here are a couple of ways to bootstrap a new project starting from this template.
The easiest way to create a new project from this template:
dotnet new install .
dotnet new webapi-undrivendev -n YourProjectName -o ./YourProjectName👉 See the complete guide: TEMPLATE-SETUP.md
Probably the best way to bootstrap this project, with just one command, but some dependencies are needed.
- Make sure Python is installed
- Install cookiecutter.
- Bootstrap initial project with the following command:
cookiecutter gh:undrivendev/template-webapi-aspnet --checkout cookiecutter
You could use this project as a GitHub template and clone it in your personal account by using the Use this template green button on the top of the page.
Then you'd have to rename classes and namespaces.
When you have the project ready, it's time to create the initial migration using dotnet-ef (or if you use Rider, like me, you can try this plugin).
Here's an example command using the default solution name, if you changed it you would have to adapt it accordingly:
dotnet ef migrations add --project ./src/Infrastructure/Infrastructure.csproj --context AppDbContext --startup-project ./src/WebApi/WebApi.csproj InitialMigrationThe above migration is applied automatically during startup in the dev environment.
Enable tests in CI: the integration tests create their schema by migrating on startup, so they need at least one migration. Once you've added the migration above, uncomment the
dotnet teststep in .github/workflows/ci.yml to run the full suite on every pull request.
This template wires app.UseAuthentication() and app.UseAuthorization() into the request pipeline, but no authentication scheme is configured — until you add one, every request is anonymous and (absent any [Authorize] attributes) allowed through. Before exposing the API, register a real scheme and add authorization policies / [Authorize] attributes, e.g.:
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* authority, audience, ... */ });Like the initial migration above, this is a deliberate "you must configure it" step: the middleware ships wired so you only have to plug in your scheme and policies.
The default API endpoints should be testable from the Swagger UI.
Enjoy!
This template ships a CI workflow at .github/workflows/ci.yml that runs on every pull request: it restores and builds the solution, scans dependencies with grype, and builds the Docker image. The dotnet test step is commented out until you add your first migration (see step 2).
It does not ship a release/deployment pipeline — deploy targets vary too much to template usefully. You need to create your own: typically, on push to main, build and push the image from src/WebApi/Dockerfile to your container registry, then trigger a deploy to your host.