SoluClean.Template 1.0.0

dotnet new install SoluClean.Template@1.0.0
                    
This package contains a .NET Template Package you can call from the shell/command line.

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

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):


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 PendingConfirmed / 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.Start activity

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

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