AniketJoshi.HotChocolateDdd.Template
1.0.0
dotnet new install AniketJoshi.HotChocolateDdd.Template@1.0.0
hotchocolate-ddd-cqrs-template
Enterprise GraphQL Template for .NET HotChocolate 15 · DDD · CQRS · MediatR · Outbox Pattern · .NET 10 LTS
A production-leaning .NET 10 template for teams building GraphQL backends with HotChocolate, clean architecture, and a reliable domain event pipeline.
GraphQL is the delivery layer. Business logic stays in the application and domain layers.
Why This Exists
Most .NET GraphQL samples are useful for learning but stop short of production architecture:
| Problem | Most samples | This template |
|---|---|---|
| HotChocolate version | v13 (2023) | v15 (2025) |
| .NET version | 6 or 7 | 10 LTS |
| Resolver pattern | Business logic in resolvers | Thin resolvers → MediatR |
| Domain events | Implement INotification directly |
Pure domain, mapped in Application |
| Event reliability | Direct publish after SaveChanges | Outbox Pattern -- crash-safe |
| Architecture tests | Missing | NetArchTest enforces layers |
| DataLoader | Missing or incomplete | Shown with N+1 proof |
| Field authorization | Missing | Field-level example included |
Architecture
GraphQL Request
|
v
HotChocolate v15 Resolver
(thin -- sends MediatR command or query, nothing else)
|
v
MediatR Pipeline
|-- LoggingBehavior
|-- ValidationBehavior (FluentValidation)
'-- DomainEventDispatchBehavior <-- fires AFTER SaveChanges
|
v
Command / Query Handler
|
v
Domain Aggregate Root
(all business logic lives here)
| raises
v
IDomainEvent
(pure C# -- zero framework dependencies in Domain layer)
| saved atomically with aggregate
v
Outbox Table
|
v
Background Processor -> INotificationHandler
See docs/architecture.md for full Mermaid diagrams.
The Outbox Pattern -- Why It Matters
Most templates do this:
// SILENT DATA LOSS
// If the app crashes between these two lines, the event is gone forever.
// No exception. No log entry. Silent.
await _dbContext.SaveChangesAsync();
await _messageBus.PublishAsync(new ProductCreatedEvent(product.Id));
This template does this:
// CRASH-SAFE
// OutboxMessage saves in the SAME database transaction as the aggregate.
// If the app crashes, the background processor picks it up on restart.
// Nothing is lost.
await _dbContext.SaveChangesAsync(); // saves Product + OutboxMessage atomically
// OutboxProcessor (background service) reads pending messages and dispatches them.
// Guaranteed at-least-once delivery.
See docs/outbox-pattern.md for the full implementation walkthrough.
Domain Events Without MediatR
Most templates couple domain events to MediatR:
// Domain layer now depends on an external framework
public class ProductCreatedEvent : INotification { ... }
This template keeps the domain pure:
// Domain layer has ZERO external dependencies
public class ProductCreatedEvent : IDomainEvent { ... }
// Application layer maps to INotification before dispatch
internal sealed class ProductCreatedNotification : INotification
{
public ProductCreatedNotification(ProductCreatedEvent domainEvent) { ... }
}
Your domain is testable without any framework installed. See docs/decisions/ADR-001-domain-events-not-mediatr.md.
Thin Resolver Pattern (HC v15)
// This is ALL the resolver does.
[QueryType]
public sealed class ProductQueries
{
public async Task<ProductDto> GetProductByIdAsync(
Guid id,
ISender sender,
CancellationToken cancellationToken)
=> await sender.Send(new GetProductByIdQuery(id), cancellationToken);
}
// Business logic lives in the handler, not the resolver.
internal sealed class GetProductByIdQueryHandler
: IRequestHandler<GetProductByIdQuery, ErrorOr<ProductDto>>
{
public async Task<ErrorOr<ProductDto>> Handle(
GetProductByIdQuery request,
CancellationToken cancellationToken)
{
var product = await _repository.GetByIdAsync(request.Id, cancellationToken);
return product is null
? CatalogErrors.ProductNotFound(request.Id)
: product.ToDto();
}
}
Field-Level Authorization
// Product.CostPrice is only visible to users with the inventory-manager policy.
// Unauthorized users get null with an AUTH_NOT_AUTHORIZED error extension.
descriptor.Field(product => product.CostPrice)
.Authorize(CatalogAuthorizationPolicies.InventoryManager);
See docs/authorization.md for the full walkthrough.
Quick Start
# Install as dotnet new template
dotnet new install AniketJoshi.HotChocolateDdd.Template
# Create new project
dotnet new hc-ddd -n MyProject
# Start dependencies
cd MyProject
docker-compose up -d
# Run
dotnet run --project src/Api
# Open GraphQL IDE
# http://localhost:5159/graphql
Or clone directly:
git clone https://github.com/aniketljoshi/hotchocolate-ddd-cqrs-template
cd hotchocolate-ddd-cqrs-template
docker-compose up -d
dotnet run --project src/Api
Project Structure
src/
|-- Domain/ <-- Pure C#. Zero external dependencies.
| AggregateRoot, ValueObject, IDomainEvent.
|
|-- Application/ <-- MediatR commands, queries, pipeline behaviors.
| DomainEventDispatchBehavior wires Outbox after SaveChanges.
|
|-- Infrastructure/ <-- EF Core, OutboxProcessor, repositories.
| ApplicationDbContext intercepts SaveChanges -> writes OutboxMessage.
|
'-- Api/ <-- HotChocolate v15 resolvers (thin layer only).
Types, Inputs, Payloads, DataLoaders, Authorization.
tests/
|-- Domain.Tests/ <-- pure unit tests, zero infrastructure
|-- Application.Tests/ <-- handler tests with mocks
|-- Architecture.Tests/ <-- NetArchTest enforces layer boundaries
'-- Integration.Tests/ <-- full GraphQL request -> Postgres via Testcontainers
What's Included
| Feature | Status |
|---|---|
| HotChocolate v15 | Done |
| .NET 10 LTS | Done |
| MediatR pipeline (validation + logging + domain events) | Done |
| DDD aggregate roots with domain event collection | Done |
| Outbox Pattern (background processor) | Done |
| ErrorOr result pattern -- no exceptions as flow control | Done |
| FluentValidation in MediatR pipeline | Done |
| DataLoaders -- N+1 problem solved + test proving it | Done |
| Field-level authorization example | Done |
| Architecture tests (NetArchTest) | Done |
| Integration tests (Testcontainers + real Postgres) | Done |
| OpenTelemetry hooks (GraphQL → MediatR → Outbox) | Done |
dotnet new template on NuGet |
Done |
| Architecture Decision Records (ADRs) | Done |
Comparison
| This template | Conference Planner | Jason Taylor | |
|---|---|---|---|
| HotChocolate version | v15 | v13 | No GraphQL |
| .NET version | 10 LTS | 6 | 9 |
| Outbox Pattern | Yes | No | No |
| Pure domain events | Yes | No | Partial |
| Architecture tests | Yes | No | Yes |
| DataLoader with N+1 test | Yes | Partial | No |
| Field authorization | Yes | No | No |
dotnet new on NuGet |
Yes | No | Yes |
| Last updated | 2025 | 2023 | Active |
Roadmap
- HotChocolate v15 + .NET 10 LTS
- Outbox Pattern
- Architecture tests
- Domain events without MediatR dependency
-
dotnet newtemplate packaging on NuGet - Field-level authorization example
- Architecture Decision Records (ADRs)
- Keycloak / OIDC auth sample
- Subscriptions backed by domain events
- OpenTelemetry full trace with Jaeger screenshot
- Second bounded context example
Contributing
Contributions are welcome. Please check the open issues before starting work.
Good entry points: good first issue
See CONTRIBUTING.md for guidelines.
Author
Aniket Joshi -- Software Developer | Solution Designer | Software Architect
aniketj.dev · LinkedIn · GitHub
Built from production experience shipping HotChocolate + DDD at enterprise scale.
⭐ If this saved you time, a star helps other .NET developers find it.
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 | 360 | 3/18/2026 |