A production-ready template for building .NET 10 Minimal APIs following Clean Architecture, Domain-Driven Design (DDD), and CQRS — without any external mediator libraries.
- Overview
- Architecture
- Project Structure
- Design Patterns
- Technologies
- Prerequisites
- Getting Started
- API Endpoints
- Authentication
- Adding a New Use Case
- License
This repository provides a clean, minimal, and opinionated starting point for ASP.NET Core APIs. It demonstrates how to combine Minimal APIs with architectural best practices — keeping the codebase simple, testable, and easy to extend — without relying on third-party mediator packages.
The solution is organized into 3 layers, each with a strict dependency rule: outer layers depend on inner layers, never the other way around.
The Api project has dependencies on both Infrastructure and Core.
The Infrastructure project has dependencies on Core.
The Core project has zero dependencies on Infrastructure or any framework-specific package.
Each layer only references layers closer to the center. Concrete types (e.g., AppDbContext, WeatherForecastRepository) are never referenced by the Core project or the endpoint definitions — they are resolved through DI at startup.
- Encapsulated entities:
WeatherForecastexposesprivate setproperties and a staticCreate(...)factory method that enforces invariants at construction time. - Repository interface in the Domain:
IWeatherForecastRepositoryis owned by the domain layer, not by Infrastructure. - No anemic model: entity creation logic lives inside the entity itself, not in a service.
Commands and queries are fully separated using two lightweight generic interfaces defined in Application/Abstractions:
public interface IQueryHandler<TQuery, TResult> { Task<TResult> HandleAsync(TQuery query); }
public interface ICommandHandler<TCommand, TResult> { Task<TResult> HandleAsync(TCommand command); }Each use case is a single, focused class. Endpoints inject only the handler they need:
// Query
app.MapGet("/weatherforecast", async (IQueryHandler<GetAllWeatherForecastsQuery, IEnumerable<WeatherForecast>> handler) => Results.Ok(await handler.HandleAsync(new GetAllWeatherForecastsQuery())));
// Command
app.MapPost("/weatherforecast", async (CreateWeatherForecastCommand command, ICommandHandler<CreateWeatherForecastCommand, WeatherForecast> handler) => { var created = await handler.HandleAsync(command); return Results.Created($"/weatherforecast/{created.Id}", created); });Each layer registers its own services through dedicated extension methods, keeping Program.cs clean:
builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration);| Technology | Version | Purpose |
| .NET | 10 | Runtime & SDK |
| ASP.NET Core Minimal APIs | 10 | HTTP layer |
| Entity Framework Core | 10 | ORM |
| SQL Server | — | Relational database |
| Scalar | – | API documentation |
- .NET 10 SDK
- A running SQL Server instance (local or remote)
- EF Core CLI tools dotnet tool install --global dotnet-ef
git clone https://github.com/RobertoFalconi/MinimalAPIsTemplate.git This is a project with a code-first approach. DB should be created on the first run. You can edit MinimalAPIsAndCleanArchitecture/appsettings.json to change the ConnectionString:
{ "ConnectionStrings": { "DefaultConnection": "Server=YOUR_SERVER;Database=WeatherDb;Trusted_Connection=True;TrustServerCertificate=True;" } }dotnet ef database update --project MinimalAPIsAndCleanArchitecture.Infrastructure --startup-project MinimalAPIsAndCleanArchitecturedotnet run --project MinimalAPIsAndCleanArchitectureThe Scalar API Reference opens automatically at http://localhost:5179/scalar/v1 in the browser on startup (development mode).
| Method | Route | Auth required | Description | Request body |
|---|---|---|---|---|
POST |
/auth/token |
✗ | Returns a JWT Bearer token | { "username": "string", "password": "string" } |
GET |
/weatherforecast |
✔ | Returns all weather forecasts | — |
POST |
/weatherforecast |
✔ | Creates a new weather forecast | { "date": "2026-03-04", "temperatureC": 22, "summary": "Warm" } |
Request
{ "date": "2026-03-04", "temperatureC": 22, "summary": "Warm" }Response 201 Created
{ "id": 1, "date": "2026-03-04", "temperatureC": 22, "temperatureF": 71, "summary": "Warm" }The API uses JWT Bearer authentication. Protected endpoints require a valid token in the Authorization header.
Call POST /auth/token with valid credentials:
POST /auth/token
Content-Type: application/json
{
"username": "",
"password": ""
}Response 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
⚠️ Demo credentials are/. ReplaceGenerateTokenCommandHandlerwith a real user store before going to production.
Add the token to every subsequent request via the Authorization header:
GET /weatherforecast
Authorization: Bearer <token>- Open
http://localhost:5179/scalar/v1 - Add "Authorization" in the headers with "Authorization" as key and paste the token in the value field with "Bearer " prefix
The CQRS structure makes it straightforward to add new features without touching existing code (Open/Closed Principle).
Example: delete a forecast by ID
// Core/Application/Commands/DeleteWeatherForecastCommand.cs
public record DeleteWeatherForecastCommand(int Id);// Core/Domain/Interfaces/IWeatherForecastRepository.cs
Task DeleteAsync(int id);// Core/Application/Commands/DeleteWeatherForecastCommandHandler.cs
public class DeleteWeatherForecastCommandHandler(IWeatherForecastRepository repository) : ICommandHandler<DeleteWeatherForecastCommand, bool> { public async Task<bool> HandleAsync(DeleteWeatherForecastCommand command) { await repository.DeleteAsync(command.Id); return true; } }// Core/DependencyInjection.cs
services.AddScoped< ICommandHandler<DeleteWeatherForecastCommand, bool>, DeleteWeatherForecastCommandHandler>();// Endpoints/WeatherForecastEndpoint.cs
app.MapDelete("/weatherforecast/{id:int}", async (int id, ICommandHandler<DeleteWeatherForecastCommand, bool> handler) => { await handler.HandleAsync(new DeleteWeatherForecastCommand(id)); return Results.NoContent(); }).WithName("DeleteWeatherForecast");This project is licensed under the MIT License.