JohnVo.ProjectTemplates
1.0.1
dotnet new install JohnVo.ProjectTemplates@1.0.1
JohnVo.ProjectTemplates
Opinionated .NET 8+ DDD + Clean Architecture + Vertical Slice/CQRS templates by JohnVo.
The generated backend keeps Domain framework-light, uses Minimal APIs + Mediator, and follows practical Filter-style conventions: Command Models, Query Projections, Specification + UnitOfWork, repository mapping expressions, Result/API envelopes, FluentValidation, permission authorization, and feature-oriented frontends.
The default sample is a small Mini Store Admin rather than a toy Todo app. It gives a generated project realistic aggregates, cross-feature queries, permissions, file uploads, reporting, pagination, concurrency handling, and a usable React/Angular workspace without turning the starter into a full ERP.
Install locally
dotnet new install ./templates/clean-api
Generate
dotnet new jv-api -n MyApp
Full example:
dotnet new jv-api -n MyApp \
--framework net9.0 \
--frontend react \
--database postgresql \
--redis true \
--minio true \
--ai gemini \
--filter true \
--otel true \
--docker true
Options:
--framework net8.0|net9.0|net10.0(defaultnet10.0)--frontend none|angular|react(defaultnone)--database sqlserver|postgresql(defaultsqlserver)--redis true|false(defaultfalse)--minio true|false(defaultfalse)--ai gemini|openai|none(defaultgemini)--filter true|false(defaultfalse)--otel true|false(defaultfalse) — Aspire AppHost + ServiceDefaults with OpenTelemetry traces, metrics and logs--docker true|false(defaultfalse)
Always included: DDD primitives, Specification, Repository/ReadOnlyRepository, UnitOfWork, Mediator, Minimal APIs, JWT + rotating HttpOnly refresh token, permission policies, FluentValidation, startup migrations, profile management, Mini Store business modules, CI, justfile, and Git initialization.
Mini Store modules
The generated starter includes:
- Dashboard — revenue/order/product/employee/low-stock snapshot, top products and recent orders.
- Products — SKU, pricing, stock, active state and pagination.
- Orders — product snapshots, guarded status transitions, stock decrement/restore and domain events.
- Employees — reuses
AppUser; Admin/Manager access is permission-based. - Reports — revenue, orders by status and top products.
- Settings — store identity, currency, timezone and low-stock threshold.
- Profile/Auth — login/register/refresh/logout and profile update.
Roles are Admin, Manager, and Staff, while API/UI authorization is enforced through permissions such as products.update, orders.cancel, employees.view, reports.view, and settings.update rather than hard-coded role checks.
Dashboard and Reports read operational data directly. Orders, Dashboard and Reports are intentionally not wired to Redis caching, avoiding stale stock/order/report data. Redis remains an optional infrastructure capability for application-specific extensions.
Filter-style application conventions
Read and write shapes are intentionally separated:
Command -> *Model -> Aggregate
Query -> Specification -> MappingExpression -> *Projection
For example, Products use command models and query projections while repository selectors remain EF-translatable:
CreateProductCommand(ProductModel Model)
UpdateProductCommand(Guid Id, ProductModel Model)
GetProductsQuery -> ProductProjection
The repository projects in SQL rather than loading aggregates and mapping them in memory.
When --filter true, Products, Orders and Employees accept LHS-bracket filter/search/sort parameters and evaluate them over the projected query before pagination. When filtering is disabled, those lists use normal page/page-size pagination.
Concurrency and stock safety
Product carries a provider-independent Guid concurrency stamp configured as an EF Core concurrency token. Product updates and stock changes rotate the stamp. Concurrent stale writes therefore raise DbUpdateConcurrencyException; UnitOfWork translates that to PersistenceConcurrencyException, and the API returns HTTP 409 Conflict.
Order creation decrements stock transactionally, while cancellation restores it. The concurrency token prevents two stale order flows from silently overwriting the same product stock value.
FluentValidation
FluentValidation is always generated. Validators are discovered from Application and executed through a Mediator pipeline behavior before handlers. Validation failures become HTTP 400 Problem Details.
Configuration and .env
The .NET backend uses standard ASP.NET Core configuration providers only. It does not parse a project .env file and does not know Docker-facing aliases such as JWT_ISSUER or GEMINI_API_KEY.
When --docker true, the template generates .env.example. Copy it to .env for Docker Compose interpolation:
JWT_ISSUER=TemplateApp
JWT_KEY=change-me
GEMINI_API_KEY=your-key
GEMINI_MODEL=gemini-3.8-flash
Compose maps those aliases to canonical ASP.NET keys inside the API container:
JWT_ISSUER -> Jwt__Issuer
JWT_KEY -> Jwt__Key
GEMINI_API_KEY -> Gemini__ApiKey
GEMINI_MODEL -> Gemini__Model
For direct backend development, use appsettings.Development.json, dotnet user-secrets, or canonical environment variables such as:
ConnectionStrings__Default
Jwt__Issuer
Jwt__Key
Gemini__ApiKey
OpenAI__ApiKey
Redis__Configuration
Minio__Endpoint
Frontend-specific environment variables belong to the frontend project rather than the backend configuration pipeline.
Aspire / OpenTelemetry
--otel true generates an Aspire AppHost and ServiceDefaults project. ServiceDefaults configures OpenTelemetry logs, traces and metrics, health checks, service discovery and standard HTTP resilience. The API uses builder.AddServiceDefaults() and exposes /health plus /alive.
The application can still target .NET 8, 9 or 10. The generated C# AppHost targets .NET 10 because current Aspire 13 tooling requires the .NET 10 SDK to run the AppHost.
Without Docker
Run:
just run
When observability is enabled, just run starts src/TemplateApp.AppHost. Aspire starts the API and its dashboard, and automatically injects the OTLP endpoint/authentication settings into the API resource. The generated local profile uses:
Dashboard: http://localhost:18888
OTLP/gRPC: http://localhost:4317
You do not need to manually set OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_HEADERS when launching through the AppHost.
With Docker
When both --docker true --otel true are selected, development Compose also starts the official standalone Aspire Dashboard container. The API exports telemetry over the Docker network:
OTEL_SERVICE_NAME=TemplateApp.Api
OTEL_EXPORTER_OTLP_ENDPOINT=http://aspire-dashboard:18889
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_EXPORTER_OTLP_HEADERS=
The dashboard UI is available at http://localhost:18888. Host ports 4317 and 4318 are also mapped for OTLP/gRPC and OTLP/HTTP. These ports bind to localhost only.
The bundled dashboard is development-only and stores telemetry in memory. compose.prod.yaml does not run an Aspire Dashboard. Production keeps standard OTEL_* passthrough so you can point the API at an external collector or observability platform.
MinIO: avatar and product image
Profile is always included:
GET /api/profile
PUT /api/profile
When --minio true, real multipart upload is added for the profile avatar and directly to Product create/update:
GET /api/profile/avatar
POST /api/profile/avatar
POST /api/products
PUT /api/products/{id}
The avatar endpoint uses multipart field file. Product create/update use normal Product form fields plus an optional multipart field named image. JPEG, PNG and WebP are accepted up to 5 MB and are validated by both MIME type and file signature.
Each Product owns at most one image object key; there is no separate Product Images resource or image-management endpoint. Creating a Product may include an image. Updating a Product without a new image keeps the existing object untouched. When a replacement is supplied, the new object is uploaded first, the Product reference is persisted, then the previous object is deleted. A failed persistence step compensates by deleting the newly uploaded object. Deleting the Product also performs best-effort storage cleanup.
Storage-specific code and the Product image picker are generated only when MinIO is enabled. Profile avatar UI is likewise generated only for MinIO builds.
Frontend starter
React and Angular use a feature-oriented layout:
app/ # application composition / shell
core/ # API, list-query and auth infrastructure
components/ # reusable UI
feedback/ # notifications / user feedback
utils/ # generic formatting helpers
features/ # business capabilities
auth/
business/
profile/
ai/ # optional
pagination.ts
pagination.css
There is intentionally no generic shared/ catch-all. Business behavior stays with its owning feature; only genuinely cross-cutting infrastructure and reusable UI are promoted to focused top-level or core concerns.
The workspace includes a permission-aware sidebar and screens for Dashboard, Products, Orders, Employees, Reports, Settings and Profile. AI remains optional. Dashboard data is fetched directly and the Angular workspace refreshes its operational snapshot periodically; Reports are fetched fresh rather than cached.
If a frontend is generated:
cd frontend
npm install
npm run api:generate
npm run dev # React
# npm start # Angular
Orval remains available through npm run api:generate.
Migrations
TemplateApp.Migrations is a design-time executable host and the only runtime-independent location that needs Microsoft.EntityFrameworkCore.Design. Migration files are emitted into Infrastructure so the API can discover/apply them without referencing the Design package.
just migrate InitialCreate
just db-update
The API applies pending migrations on startup by default (Database__AutoMigrate=true). Docker Compose maps AUTO_MIGRATE from .env to that canonical ASP.NET key. Startup retry applies only to transient connectivity/startup errors; schema/migration errors fail fast.
Docker and just
just docker-up-d
just docker-up-d api db
just docker-logs api
just docker-stop api db
just docker-restart api db
just docker-clean
docker-clean intentionally resets the full development stack including named volumes.
CI / generated-combination smoke tests
Repository CI generates and builds representative combinations including:
- .NET 8 SQL Server backend
- .NET 9 default backend
- filter-disabled backend
- React full stack with PostgreSQL + Redis + MinIO + Gemini + LHS filtering + OpenTelemetry + Docker
- Angular with PostgreSQL + MinIO + LHS filtering
Guards reject legacy Todo starter artifacts, filter infrastructure when filtering is disabled, backend .env alias leakage, and cache dependencies inside Orders/Dashboard/Reports.
Git
dotnet new jv-api runs git init -b main as a template post-action. .gitignore is included.
Pack
dotnet pack JV.ProjectTemplates.csproj -c Release
dotnet new install ./bin/Release/JohnVo.ProjectTemplates.1.0.0.nupkg
NuGet publish
The nuget-publish GitHub Actions workflow can be started manually or by pushing a semantic-version tag such as v1.0.0. It packs the template, installs the produced .nupkg, generates a full-stack smoke project, builds/tests the backend, builds the frontend, and only then pushes to NuGet.org.
Publishing uses NuGet Trusted Publishing (GitHub OIDC) rather than a long-lived API key. Configure a nuget.org Trusted Publishing policy for repository owner johnvo402, repository project-templates, and workflow file nuget-publish.yml. Leave the policy environment empty unless the workflow is later moved behind a GitHub Environment. In GitHub Actions, add repository variable NUGET_USER containing the nuget.org username/profile name that owns the policy. No NUGET_API_KEY secret is required.
-
net8.0
- 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.