A modern .NET 10 REST API template implementing Vertical Slice Architecture with CQRS pattern, minimal APIs, FluentValidation, and Entity Framework Core with PostgreSQL.
- .NET 10 - Latest .NET runtime
- ASP.NET Core Minimal APIs - Lightweight HTTP endpoints
- Entity Framework Core 10.0.3 - ORM for database access
- PostgreSQL - Database provider (Npgsql)
- FluentValidation 12.1.1 - Request validation
- CQRS Pattern - Command Query Responsibility Segregation
- Vertical Slice Architecture - Feature-driven organization
- Pipeline Decorators - Logging and validation decorators
- Scalar UI - OpenAPI documentation viewer
VerticalSliceArchitectureTemplate/
├── Abstractions/ # Core interfaces and abstractions
│ ├── IApiEndpoint.cs # Endpoint contract
│ ├── IHandler.cs # Handler contract
│ ├── Result.cs # Success/Failure result wrapper
│ └── Errors/
│ └── Error.cs # Error type definition
│
├── Constants/ # Shared constants
│ └── ApiTags.cs # OpenAPI tags
│
├── Database/
│ └── ApplicationDbContext.cs # EF Core DbContext
│
├── Entities/
│ └── Book.cs # Domain entity
│
├── Extensions/ # DI & configuration extensions
│ ├── DatabaseExtensions.cs
│ ├── HandlerRegistrationExtensions.cs
│ ├── HealthChecksExtensions.cs
│ ├── MapEndpointExtensions.cs
│ └── ResultExtensions.cs
│
├── Features/ # Vertical slices (features)
│ └── BookFeature/
│ ├── BookErrors.cs # Feature-specific errors
│ ├── CreateBook/
│ │ ├── CreateBookRequest.cs (record)
│ │ ├── CreateBookResponse.cs (record)
│ │ ├── CreateBookHandler.cs
│ │ ├── CreateBookValidator.cs
│ │ └── CreateBookEndpoint.cs
│ ├── GetAllBooks/
│ │ ├── GetAllBooksRequest.cs (record)
│ │ ├── GetAllBooksResponse.cs (record)
│ │ ├── BookDto.cs (record)
│ │ ├── GetAllBooksHandler.cs
│ │ ├── GetAllBooksValidator.cs
│ │ └── GetAllBooksEndpoint.cs
│ ├── GetBookById/
│ │ ├── GetBookByIdRequest.cs (record)
│ │ ├── GetBookByIdResponse.cs (record)
│ │ ├── GetBookByIdHandler.cs
│ │ ├── GetBookByIdValidator.cs
│ │ └── GetBookByIdEndpoint.cs
│ ├── UpdateBook/
│ │ ├── UpdateBookRequest.cs (record)
│ │ ├── UpdateBookResponse.cs (record)
│ │ ├── UpdateBookHandler.cs
│ │ ├── UpdateBookValidator.cs
│ │ └── UpdateBookEndpoint.cs
│ └── DeleteBook/
│ ├── DeleteBookRequest.cs (record)
│ ├── DeleteBookResponse.cs (record)
│ ├── DeleteBookHandler.cs
│ ├── DeleteBookValidator.cs
│ └── DeleteBookEndpoint.cs
│
├── Pipelines/ # Request processing decorators
│ ├── ValidationDecorator.cs # FluentValidation pipeline
│ └── LoggingDecorator.cs # Logging pipeline
│
├── Repository/ # Data access layer
│ ├── IRepository.cs
│ ├── IUnitOfWork.cs
│ ├── Repository.cs
│ └── UnitOfWork.cs
│
├── appsettings.json # Configuration
├── appsettings.Development.json
├── Program.cs # App startup & DI setup
└── VerticalSliceArchitectureTemplate.csproj
Each feature is self-contained with its own request/response DTOs, handler, validator, and endpoint:
- Independent - Changes to one feature don't affect others
- Scalable - Easy to add new features
- Testable - Each slice can be tested in isolation
- Commands - Operations that modify state (Create, Update, Delete)
- Queries - Operations that read data (GetAll, GetById)
- Handlers -
IHandler<TRequest, TResponse>processes each request
All DTOs use C# records for immutability and cleaner syntax:
public sealed record CreateBookRequest(string Title, string Author, string ISBN, decimal Price, int PublishedYear);
public sealed record CreateBookResponse(Guid Id, string Title, string Author, string ISBN, decimal Price, int PublishedYear);FluentValidation decorators automatically validate requests before handlers:
RuleFor(c => c.Title)
.NotEmpty().WithMessage("Title is required")
.MaximumLength(200).WithMessage("Title must not exceed 200 characters");Decorators log request/response and execution time:
LoggingDecorator → Logs request, response, duration
ValidationDecorator → Validates request
Handler → Processes actual business logicCentralized error handling with Result<T> wrapper:
// Success
return Result.Success(new CreateBookResponse(...));
// Failure
return Result.Failure<CreateBookResponse>(BookErrors.NotFound(id));| Package | Version | Purpose |
|---|---|---|
| Microsoft.EntityFrameworkCore | 10.0.3 | ORM & data access |
| Microsoft.EntityFrameworkCore.Design | 10.0.3 | EF Core tools support |
| Microsoft.EntityFrameworkCore.Sqlite | 10.0.3 | SQLite provider |
| Microsoft.EntityFrameworkCore.SqlServer | 10.0.3 | SQL Server provider |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 | PostgreSQL provider |
| Microsoft.EntityFrameworkCore.Tools | 10.0.3 | Migrations & scaffolding |
| FluentValidation | 12.1.1 | Request validation |
| FluentValidation.DependencyInjectionExtensions | 12.1.1 | DI integration |
| Scalar.AspNetCore | 2.12.40 | OpenAPI UI |
| Scrutor | 7.0.0 | Assembly scanning for DI |
| AspNetCore.HealthChecks.UI.Client | 9.0.0 | Health checks |
| Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore | 10.0.3 | DB health checks |
Connection String (appsettings.json):
{
"ConnectionStrings": {
"connection": "Host=localhost;Port=5432;Database=VerticalSlice;Username=postgres;Password=your_password"
}
}Package Manager Console (Visual Studio):
Add-Migration InitialCreate -Project VerticalSliceArchitectureTemplate
Update-Database -Project VerticalSliceArchitectureTemplate1. Create Initial Migration:
dotnet ef migrations add InitialCreate -p VerticalSliceArchitectureTemplate2. Apply Migration to Database:
dotnet ef database update -p VerticalSliceArchitectureTemplate3. Add New Migration (after model changes):
dotnet ef migrations add MigrationName -p VerticalSliceArchitectureTemplate
dotnet ef database update -p VerticalSliceArchitectureTemplate4. Revert Last Migration:
dotnet ef database update LastGoodMigration -p VerticalSliceArchitectureTemplate
dotnet ef migrations remove -p VerticalSliceArchitectureTemplateFor SQL Server:
// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("connection"));
});For SQLite (Development):
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("connection"));
});1. Install Dependencies:
dotnet restore2. Build Project:
dotnet build3. Run Application:
dotnet runScalar is a modern, beautiful alternative to Swagger UI for testing and exploring APIs.
- URL:
https://localhost:5001/scalar/v1 - OpenAPI Spec:
https://localhost:5001/openapi/v1.json
✨ Scalar provides:
- 🚀 Beautiful, modern UI for API documentation
- 📝 Interactive request/response testing
- 🔄 Request history
- 📦 Request/response examples with syntax highlighting
- 🏷️ Endpoint organization by tags (our
ApiTags.Books) - 📱 Mobile-responsive design
- 🔐 Support for authentication headers
- 💾 Request templates and presets
- Open Scalar: Navigate to
https://localhost:5001/scalar/v1 - Select Endpoint: Click any book endpoint under the "books" tag
- Enter Request Data: Fill in parameters and request body
- Execute Request: Click "Send" button
- View Response: See status code, headers, and response body
In Scalar UI:
Endpoint: POST /books
Request Body:
{
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "9780132350884",
"price": 39.99,
"publishedYear": 2008
}
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "9780132350884",
"price": 39.99,
"publishedYear": 2008
}
Health checks monitor the application and dependent services (database, external APIs, etc.).
// Program.cs
builder.Services
.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();URL: https://localhost:5001/health
Response (Healthy):
GET /health
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "Healthy",
"timestamp": "2026-02-23T10:30:00Z",
"checks": {
"ApplicationDbContext": {
"status": "Healthy",
"description": "Database connection successful"
}
}
}Response (Unhealthy):
HTTP/1.1 503 Service Unavailable
{
"status": "Unhealthy",
"timestamp": "2026-02-23T10:31:00Z",
"checks": {
"ApplicationDbContext": {
"status": "Unhealthy",
"description": "Database connection failed"
}
}
}Map Health Check Endpoint:
// In Program.cs after building app
var app = builder.Build();
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = WriteHealthCheckResponse
});
// Custom response formatter
static async Task WriteHealthCheckResponse(
HttpContext httpContext,
HealthReport report)
{
httpContext.Response.ContentType = "application/json";
var response = new
{
status = report.Status.ToString(),
timestamp = DateTime.UtcNow,
checks = report.Entries.ToDictionary(
e => e.Key,
e => new
{
status = e.Value.Status.ToString(),
description = e.Value.Description
})
};
await httpContext.Response.WriteAsJsonAsync(response);
}| Status | HTTP Code | Meaning |
|---|---|---|
Healthy |
200 | All checks passed |
Degraded |
200 | Some checks passed, others degraded |
Unhealthy |
503 | One or more checks failed |
✅ Do:
- Check critical dependencies (database, cache, external APIs)
- Set reasonable timeouts for checks
- Include descriptive status messages
- Log health check failures
- Use health checks in load balancers
- Implement liveness & readiness probes
❌ Don't:
- Make health checks too complex
- Hit external APIs on every health check
- Override built-in status codes
- Ignore degraded status
- Leave health checks unconfigured
All book endpoints are tagged with "books" in OpenAPI documentation.
POST /books
Content-Type: application/json
{
"title": "The Art of Programming",
"author": "John Doe",
"isbn": "9781234567890",
"price": 49.99,
"publishedYear": 2023
}
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "The Art of Programming",
"author": "John Doe",
"isbn": "9781234567890",
"price": 49.99,
"publishedYear": 2023
}GET /books
Response 200:
{
"books": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "The Art of Programming",
"author": "John Doe",
"isbn": "9781234567890",
"price": 49.99,
"publishedYear": 2023
}
]
}GET /books/550e8400-e29b-41d4-a716-446655440000
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "The Art of Programming",
"author": "John Doe",
"isbn": "9781234567890",
"price": 49.99,
"publishedYear": 2023
}
Response 404:
{
"code": "Books.NotFound",
"description": "The Book with Id '550e8400-e29b-41d4-a716-446655440000' was not found"
}PUT /books/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"title": "Advanced Programming",
"author": null,
"isbn": null,
"price": 59.99,
"publishedYear": null
}
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Advanced Programming",
"author": "John Doe",
"isbn": "9781234567890",
"price": 59.99,
"publishedYear": 2023
}DELETE /books/550e8400-e29b-41d4-a716-446655440000
Response 200:
{
"id": "550e8400-e29b-41d4-a716-446655440000"
}
Response 404:
{
"code": "Books.NotFound",
"description": "The Book with Id '550e8400-e29b-41d4-a716-446655440000' was not found"
}Invalid Create Request:
POST /books
{
"title": "",
"author": "John Doe",
"isbn": "invalid",
"price": -10,
"publishedYear": 2050
}
Response 400:
{
"errors": [
{
"code": "NotEmptyValidator",
"description": "Title is required",
"type": 2
},
{
"code": "NotEmptyValidator",
"description": "Author is required",
"type": 2
},
{
"code": "NotEmptyValidator",
"description": "ISBN is required",
"type": 2
},
{
"code": "GreaterThanValidator",
"description": "Published year must be a valid year",
"type": 2
}
],
"code": "Validation.General",
"description": "One or more validation errors occurred",
"type": 2
}Global exception handling ensures all unhandled exceptions are caught and returned as consistent API responses.
ASP.NET Core 10 provides IExceptionHandler for global exception handling:
// Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler();Global exception handling can be implemented using IExceptionHandler to catch and format all unhandled exceptions:
// Register in Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler();| Exception Type | Status Code | Example |
|---|---|---|
ValidationException |
400 Bad Request | FluentValidation errors |
KeyNotFoundException |
404 Not Found | Resource not found |
ArgumentException |
400 Bad Request | Invalid input argument |
UnauthorizedAccessException |
401 Unauthorized | Authentication failed |
InvalidOperationException |
409 Conflict | Operation cannot be performed |
| Default Exception | 500 Internal Server Error | Unhandled errors |
Global exception handlers are configured in Program.cs using AddExceptionHandler<>() and UseExceptionHandler() middleware.
Database Connection Error:
POST /books
{
"title": "Test",
"author": "Author",
"isbn": "1234567890",
"price": 19.99,
"publishedYear": 2023
}
Response 500:
{
"type": "InvalidOperationException",
"title": "An error occurred",
"status": 500,
"detail": "An exception has been raised that is likely due to a transient failure.",
"instance": "/books"
}Validation Error (caught by FluentValidation):
Response 400:
{
"errors": [
{
"code": "NotEmptyValidator",
"description": "Title is required",
"type": 2
},
{
"code": "GreaterThanValidator",
"description": "Published year must be a valid year",
"type": 2
}
],
"code": "Validation.General",
"description": "One or more validation errors occurred",
"type": 2
}Exceptions should be logged with context (path, timestamp, exception type) for debugging and monitoring purposes.
Stack traces and detailed error information should only be exposed in development environments to prevent information leakage in production.
✅ Do:
- Log all exceptions with context (path, user, timestamp)
- Return consistent error response format
- Include error codes for client handling
- Hide sensitive information in production
- Handle specific exceptions first, generic last
- Use proper HTTP status codes
- Return failed operations as JSON
❌ Don't:
- Expose stack traces to clients (except dev)
- Log sensitive data (passwords, tokens)
- Return 500 for business logic failures
- Suppress logging of exceptions
- Ignore validation errors
- Return HTML error pages from APIs
HTTP Request
↓
MinimalAPI Endpoint (Route binding)
↓
LoggingDecorator (Log request start)
↓
ValidationDecorator (FluentValidation)
↓
Handler (Business logic)
├─ IRepository (Data access)
├─ IUnitOfWork (Transaction)
└─ Returns Result<T>
↓
LoggingDecorator (Log response, duration)
↓
Endpoint Result Mapping (Map to HTTP response)
↓
[Exception Caught?]
├─ Yes → GlobalExceptionHandler
│ └─ Format & Log Error
│ └─ Return Error Response
└─ No → HTTP Response (200/400/404)
-
Create Feature Folder under
Features/BookFeature/:Features/BookFeature/NewFeature/ ├── NewFeatureRequest.cs (record) ├── NewFeatureResponse.cs (record) ├── NewFeatureHandler.cs ├── NewFeatureValidator.cs └── NewFeatureEndpoint.cs -
Implement Handler (inherit
IHandler<TRequest, TResponse>):public sealed class NewFeatureHandler( IRepository<Book> _repo) : IHandler<NewFeatureRequest, Result<NewFeatureResponse>> { public async Task<Result<NewFeatureResponse>> HandleAsync(...) { } }
-
Register Validator (auto-discovered from assembly)
-
Create Endpoint (inherit
IApiEndpoint):public void MapEndpoint(WebApplication app) { app.MapPost("path", async (...) => { ... }) .WithTags(ApiTags.Books) .Produces<NewFeatureResponse>(StatusCodes.Status200OK); }
✅ Do:
- Keep each feature independent
- Use records for immutable DTOs
- Validate at the boundary (validators)
- Use dependency injection for all services
- Log important operations
- Handle errors with Result
- Write handlers that do one thing well
❌ Don't:
- Put business logic in endpoints
- Modify requests/responses mid-pipeline
- Mix validators (create separate per feature)
- Expose entities in API responses
- Skip validation
- Ignore error codes from handlers
Enable Debug Logging:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information"
}
}
}View Database Queries:
// In Program.cs
options.LogTo(Console.WriteLine, LogLevel.Information);MIT License - Use freely and commercially.
Built with .NET 10 | Vertical Slice Architecture | CQRS Pattern