SoluClean.Template
1.0.0
dotnet new install SoluClean.Template@1.0.0
SoluClean — Clean Architecture .NET Template
A production-ready .NET 10 API template built on Clean Architecture, CQRS, Wolverine Sagas, GraphQL, gRPC, SignalR, and OpenTelemetry.
Table of Contents
- Overview
- Architecture
- Tech Stack
- Project Structure
- Prerequisites
- Installation
- Getting Started
- Template CLI Options
- API Endpoints
- GraphQL API
- Real-Time (SignalR)
- Reservation Saga — E2E Flow
- Adding a New Feature
- Observability
- Idempotency
- Author
Overview
SoluClean is a dotnet new template that gives you a fully wired, enterprise-grade API in seconds. It enforces strict layer separation (Domain → Application → Infrastructure → API), ships with a working distributed Saga as a reference implementation, and is designed to be extended, not rewritten.
Clone it, install it, scaffold your project, and start writing your domain logic — everything else is already done.
Architecture
┌─────────────────────────────────────────────────────┐
│ SoluClean.API │ ← HTTP Endpoints, GraphQL, SignalR, Wolverine HTTP
│ (Presentation / Delivery Layer) │
└──────────────────────┬──────────────────────────────┘
│ depends on
┌──────────────────────▼──────────────────────────────┐
│ SoluClean.Application │ ← CQRS Handlers, Sagas, Validators, DTOs, Interfaces
│ (Application / Use Cases Layer) │
└──────────────────────┬──────────────────────────────┘
│ depends on
┌──────────────────────▼──────────────────────────────┐
│ SoluClean.Domain │ ← Entities, Enums, Domain Contracts
│ (Core Domain Layer) │
└─────────────────────────────────────────────────────┘
▲
│ implements interfaces from Application
┌──────────────────────┴──────────────────────────────┐
│ SoluClean.Infrastructure │ ← EF Core DbContext, Migrations, gRPC Client, Services
│ (Infrastructure / Data Layer) │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ SoluClean.PaymentService │ ← Standalone gRPC Microservice
│ (External Service / Sidecar) │
└─────────────────────────────────────────────────────┘
Key principles:
- The Domain layer has zero external dependencies.
- The Application layer depends only on Domain and abstractions (interfaces).
- The Infrastructure layer implements those abstractions and wires up EF Core, gRPC, etc.
- The API layer composes everything via Dependency Injection.
Tech Stack
| Category | Library / Tool | Version |
|---|---|---|
| Runtime | .NET | 10.0 |
| Message Bus / Sagas | WolverineFx | 6.39.1 |
| ORM | Entity Framework Core + SQL Server | 10.0.12 |
| GraphQL | HotChocolate | 16.6.6 |
| Real-Time | ASP.NET Core SignalR | built-in |
| Inter-Service | gRPC (Grpc.AspNetCore / Grpc.Net.Client) | 2.62.0 |
| Validation | FluentValidation | 12.1.1 |
| Object Mapping | Riok.Mapperly (source-gen) | 4.3.1 |
| Result Pattern | FluentResults | 4.0.0 |
| Logging | Serilog (Console / File / Seq) | 10.0.0 |
| Observability | OpenTelemetry (OTLP + Prometheus) | 1.19.x |
| API Docs | Swashbuckle (Swagger) | 10.2.3 |
Project Structure
Solu-Clean/
├── Directory.Packages.props # Central NuGet version management
├── SoluClean.slnx # Solution file
└── src/
├── SoluClean.API/ # Presentation layer
│ ├── Endpoints/ # Wolverine HTTP endpoint classes
│ ├── Graphql/ # HotChocolate Query/Subscription types
│ ├── Hubs/ # SignalR hubs
│ ├── Handlers/ # Wolverine event handlers (e.g., broadcast via SignalR)
│ ├── Middlewares/ # Global exception handler
│ ├── Mappers/ # Mapperly source-gen mappers
│ ├── Requests/ # HTTP request DTOs
│ ├── Telemetry/ # OpenTelemetry setup
│ ├── Extensions/ # IHostBuilder / WebApplication extensions
│ ├── appsettings.json
│ └── Program.cs
│
├── SoluClean.Application/ # Use-cases layer
│ ├── Common/ # IAppDbContext, IPaymentClient interfaces
│ ├── Middlewares/ # Wolverine IdempotencyMiddleware
│ ├── Telemetry/ # DiagnosticsConfig (ActivitySource, Meters)
│ └── Features/
│ ├── Students/ # Example feature: Commands, Queries, DTOs
│ ├── Courses/ # Example feature
│ └── Reservations/ # Full Saga reference implementation
│ ├── Commands/ # CancelReservationCommand
│ ├── Events/ # ReservationStatusChangedEvent
│ ├── Handlers/ # ProcessPaymentHandler
│ ├── Messages/ # StartReservation, ProcessPayment, PaymentSucceeded/Failed, Timeout
│ └── Sagas/ # ReservationSaga (full distributed transaction)
│
├── SoluClean.Domain/ # Core domain (no external deps)
│ ├── Entities/ # Student, Course, Reservation, ProcessedCommand
│ ├── Enums/ # ReservationStatus
│ └── Common/ # Base classes / shared contracts
│
├── SoluClean.Infrastructure/ # Data + external services
│ ├── Data/ # AppDatabaseContext (EF Core DbContext)
│ ├── Persistence/ # Entity configurations (IEntityTypeConfiguration)
│ ├── Services/ # GrpcPaymentClient (implements IPaymentClient)
│ ├── Protos/ # payment.proto (gRPC contract)
│ ├── DependancyInjections/ # AddInfrastructureServices extension
│ └── Migrations/ # EF Core migration files
│
└── SoluClean.PaymentService/ # Standalone gRPC microservice
├── Data/ # PaymentDbContext
├── Entities/ # StudentBalance
├── Services/ # PaymentGrpcService
├── Protos/ # payment.proto (shared contract)
├── Migrations/ # EF Core migration files
└── Program.cs
Prerequisites
Make sure the following are installed before proceeding:
| Tool | Version | Purpose |
|---|---|---|
| .NET SDK | 10.0+ | Building and running the project |
| SQL Server | 2019+ or LocalDB | Primary database |
| EF Core CLI | latest | Running migrations |
Install EF Core CLI (if not already installed):
dotnet tool install --global dotnet-ef
Optional (for full observability):
- Seq — structured log viewer (
http://localhost:5341) - Prometheus + Grafana — metrics dashboard
- OpenTelemetry Collector — trace aggregator (
http://localhost:4317)
Installation
Install as a dotnet new template
From the repository root (where .template.config/ lives):
dotnet new install .
Verify the template is registered:
dotnet new list | grep soluclean
Scaffold a new project
dotnet new soluclean -n MyCompany.MyApp
This generates a fully working solution under MyCompany.MyApp/.
Getting Started
1. Configure the API
Open src/MyApp.API/appsettings.json and update the connection string:
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=MyAppDb;Integrated Security=SSPI;TrustServerCertificate=True;"
}
}
Note: If you enabled Seq logging, also set
"serverUrl"in the Serilog Seq sink.
2. Configure the Payment Service
Open src/MyApp.PaymentService/appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=MyAppPaymentDb;Integrated Security=SSPI;TrustServerCertificate=True;"
}
}
The API talks to the Payment Service over gRPC on https://localhost:7022 (HTTPS profile). To override the URL:
// src/MyApp.API/appsettings.json
{
"PaymentServiceUrl": "https://your-payment-service-host:port"
}
3. Run Migrations
Main API database (run from solution root):
dotnet ef database update \
--project src/MyApp.Infrastructure/MyApp.Infrastructure.csproj \
--startup-project src/MyApp.API/MyApp.API.csproj
Payment Service database:
dotnet ef database update \
--project src/MyApp.PaymentService/MyApp.PaymentService.csproj \
--startup-project src/MyApp.PaymentService/MyApp.PaymentService.csproj
4. Start the Services
Terminal 1 — Payment Service (must start first, API calls it via gRPC):
cd src/MyApp.PaymentService
dotnet run --launch-profile https
Payment Service will listen on:
https://localhost:7022← gRPC endpoint (used by API)http://localhost:5212
Terminal 2 — Main API:
cd src/MyApp.API
dotnet run
API will be available at:
- Swagger UI:
http://localhost:5248/swagger - GraphQL Playground:
http://localhost:5248/graphql - SignalR Hub:
ws://localhost:5248/hubs/reservation
Template CLI Options
Customize the generated project during scaffolding:
dotnet new soluclean -n MyApp [options]
| Option | Type | Default | Description |
|---|---|---|---|
--LogProvider |
Console | File | Seq |
Console |
Serilog sink to configure |
--SeqUrl |
string | http://localhost:5341 |
Seq server URL (only used when LogProvider=Seq) |
--EnableObservability |
bool | false |
Enable OpenTelemetry (tracing + metrics) |
--EnableTracing |
bool | false |
Enable distributed tracing |
--EnableMetrics |
bool | false |
Enable Prometheus metrics scraping |
--IncludeEFCoreTracing |
bool | false |
Add EF Core instrumentation to traces |
--IncludeWolverineTracing |
bool | false |
Add Wolverine activity source to traces |
--OtlpEndpoint |
string | http://localhost:4317 |
OTLP collector endpoint |
Examples:
# Minimal project — no telemetry, file logging
dotnet new soluclean -n MyApp --EnableObservability false --LogProvider File
# Full observability with Seq
dotnet new soluclean -n MyApp --LogProvider Seq --SeqUrl http://seq.myinfra.com:5341
# Point to a remote OTLP collector
dotnet new soluclean -n MyApp --OtlpEndpoint http://otel-collector:4317
API Endpoints
All endpoints are declared via Wolverine HTTP attributes — no [ApiController] boilerplate required.
Students
| Method | Route | Description |
|---|---|---|
GET |
/api/students |
List all students (paginated) |
GET |
/api/students/{id} |
Get student by ID |
POST |
/api/students |
Create a new student |
PUT |
/api/students/{id} |
Update a student |
DELETE |
/api/students/{id} |
Delete a student |
Courses
| Method | Route | Description |
|---|---|---|
GET |
/api/courses |
List all courses |
GET |
/api/courses/{id} |
Get course by ID |
POST |
/api/courses |
Create a new course |
DELETE |
/api/courses/{id} |
Delete a course |
Reservations
| Method | Route | Description | Notes |
|---|---|---|---|
POST |
/api/reservations |
Create a reservation (starts saga) | Returns 202 Accepted |
GET |
/api/reservations/{id}/status |
Poll reservation status | Pending → Confirmed / Failed / Cancelled |
PATCH |
/api/reservations/{id} |
Cancel a reservation | Idempotent |
Idempotency: Pass an optional Idempotency-Key: <guid> header on any mutating request to prevent duplicate processing.
GraphQL API
Accessible at /graphql (Banana Cake Pop IDE in development).
Queries
# Cursor-paginated list of students with projection support
query {
students(first: 10, after: "cursor") {
nodes {
id
firstName
lastName
courses {
id
name
price
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Get a single student profile by ID
query {
studentProfileById(id: "your-guid-here") {
id
firstName
lastName
email
courses {
id
name
}
}
}
# Paginated courses
query {
courses(first: 5) {
nodes {
id
name
price
}
}
}
Subscriptions (Real-Time)
subscription {
reservationStatusChanged {
reservationId
status
}
}
HotChocolate subscriptions are backed by Wolverine event publishing, which means updates are pushed the moment the saga transitions state.
Real-Time (SignalR)
Connect to the reservation hub:
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5248/hubs/reservation")
.build();
connection.on("ReservationStatusChanged", (reservationId, status) => {
console.log(`Reservation ${reservationId} → ${status}`);
});
await connection.start();
The hub broadcasts whenever a Wolverine ReservationStatusChangedEvent is handled — no polling needed.
Reservation Saga — E2E Flow
The ReservationSaga is the reference implementation of a distributed transaction using Wolverine's durable saga pattern.
Client API Wolverine PaymentService
│ │ │ │
│─ POST /reservations ──►│ │ │
│ │── PublishAsync ──────────►│ │
│◄─ 202 Accepted ───────│ StartReservation │ │
│ │ Saga.Start() │
│ │ Creates Reservation (Pending) │
│ │ Schedules Timeout (10 min) │
│ │ │── ProcessPayment ──────────►│
│ │ │ Checks balance
│ │ │◄── PaymentSucceeded / Failed│
│ │ Updates Reservation status │
│ │ Broadcasts via SignalR + GraphQL sub │
│ │ │ │
│─ GET /reservations/{id}/status ──────────────────►│ │
│◄─ { status: "Confirmed" } ────────────────────────│ │
Saga states:
| Status | Meaning |
|---|---|
Pending |
Reservation created, payment in progress |
Confirmed |
Payment succeeded |
Failed |
Payment failed (insufficient funds, student not found, etc.) |
Cancelled |
Timeout elapsed (10 minutes) before payment completed |
Adding a New Feature
Follow this pattern (using Orders as an example):
1. Domain — Add the entity
// src/MyApp.Domain/Entities/Order.cs
public class Order
{
public Guid Id { get; set; }
public decimal Total { get; set; }
// ...
}
2. Application — Define the use case
// src/MyApp.Application/Features/Orders/Commands/CreateOrderCommand.cs
public record CreateOrderCommand(Guid IdempotencyKey, decimal Total) : IIdempotentCommand;
// src/MyApp.Application/Features/Orders/Handlers/CreateOrderHandler.cs
public static class CreateOrderHandler
{
public static async Task<Result<Guid>> Handle(
CreateOrderCommand command,
IAppDbContext db,
CancellationToken ct)
{
var order = new Order { Id = Guid.NewGuid(), Total = command.Total };
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Ok(order.Id);
}
}
3. Infrastructure — Register in DbContext
// src/MyApp.Infrastructure/Data/AppDatabaseContext.cs
public DbSet<Order> Orders { get; set; }
Add and apply migration:
dotnet ef migrations add AddOrders \
--project src/MyApp.Infrastructure/MyApp.Infrastructure.csproj \
--startup-project src/MyApp.API/MyApp.API.csproj
dotnet ef database update \
--project src/MyApp.Infrastructure/MyApp.Infrastructure.csproj \
--startup-project src/MyApp.API/MyApp.API.csproj
4. API — Expose the endpoint
// src/MyApp.API/Endpoints/OrderEndpoints.cs
public static class OrderEndpoints
{
[WolverinePost("/api/orders")]
public static async Task<IResult> CreateOrder(
[FromHeader(Name = "Idempotency-Key")] Guid? idempotencyKey,
[FromBody] CreateOrderRequest request,
IMessageBus bus)
{
var result = await bus.InvokeAsync<Result<Guid>>(
new CreateOrderCommand(idempotencyKey ?? Guid.NewGuid(), request.Total));
return result.IsSuccess
? Results.Created($"/api/orders/{result.Value}", new { Id = result.Value })
: result.ToProblemDetails();
}
}
That's it — Wolverine discovers the handler automatically, validation is auto-applied, and idempotency is handled by middleware.
Observability
Structured Logging (Serilog)
All log output is enriched with MachineName, ThreadId, and the full request context.
| Profile | Output |
|---|---|
Console (default) |
Colored console output |
File |
Rolling daily files in Logs/log-<date>.txt, 7-day retention |
Seq |
Centralized log server with full-text search |
Distributed Tracing (OpenTelemetry)
The template instruments:
- ASP.NET Core HTTP requests
- HttpClient outgoing calls
- Entity Framework Core database queries
- Wolverine message handling and publishing
- Custom spans — e.g.,
ReservationSaga.Startactivity
Traces are exported via OTLP to your configured collector (Jaeger, Tempo, Zipkin, etc.).
Metrics (Prometheus)
Scraped at /metrics. Includes:
- Standard .NET runtime metrics (GC, threads, heap)
- ASP.NET Core request metrics
- Custom counters:
reservation.successes,reservation.failures
Custom Telemetry
Add your own metrics in Application/Telemetry/DiagnosticsConfig.cs:
public static readonly Counter<long> MyOperationCount =
Meter.CreateCounter<long>("myapp.my_operation.count");
Idempotency
Any command that implements IIdempotentCommand is automatically guarded against duplicate processing by the IdempotencyMiddleware:
public record CreateOrderCommand(Guid IdempotencyKey, decimal Total) : IIdempotentCommand;
The first time a command with a given IdempotencyKey is processed, it is recorded in the ProcessedCommands table. If the same key arrives again (e.g., from a retry), the middleware short-circuits execution and returns immediately — preventing double-writes.
Pass the key via HTTP header:
POST /api/orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{ "total": 99.99 }
Author
Ahmed Hany
- 🧑💻 Backend .NET Software Engineer
- 🐙 GitHub — AhmedHany140
Built with ❤️ to help .NET developers ship clean, scalable APIs faster.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 40 | 9/22/2026 |