Development Workflow¶
This page covers the day-to-day developer loop for the Cargonerds solution: prerequisites and one-time setup, the branching model, the formatting hooks (CSharpier + Prettier), the mandatory git-worktree rule, building, and running. It explains both what to do and why this repo is wired the way it is, so the pre-commit hook and CI never surprise you.
For the conceptual layout of the solution (projects, common.props, Directory.Packages.props,
the .sln/.slnx/per-module solution set), see
Solution Structure. For running the app, see
Running Locally.
Prerequisites¶
The authoritative tool list is the root README.md "Pre-requirements" section, reconciled with
what the code actually requires:
| Tool | Version | Why |
|---|---|---|
| .NET SDK | 10 (net10.0) |
All first-party projects target net10.0 (see common.props / each .csproj). CI provisions 10.0.x. |
| Node.js | 22 | The Next.js frontend/realtime app declares engines.node: 22.x; a pre-commit guard (.githooks/validate-node-version.mjs) enforces it. The README's "Node 18/20" note predates the realtime app. |
| Docker | current | Aspire spins up SQL Server / Redis / RabbitMQ containers for local runs, and Testcontainers spins up SQL Server / Redis / RabbitMQ / Azurite for the integration tests. |
PowerShell 7 (pwsh) |
current | Required by .githooks/install.ps1 and the deploy scripts under .scripts/. Ships natively on Windows; install PowerShell Core on Linux/macOS. |
| Signing certificate | — | openiddict.pfx for production OpenIddict. See the README for the dotnet dev-certs / openssl commands. |
The README and .abpsln understate the framework version
The root README.md says ".NET 9.0+" and Cargonerds.abpsln still records
AbpFramework 10.0.0 / LeptonX 5.0.0 / net9.0. These are stale. The build truth is
common.props: <AbpVersion>10.1.1</AbpVersion>, <LeptonXVersion>5.1.1</LeptonXVersion>,
and every project's TargetFramework is net10.0. Always trust common.props and the
.csproj over the README/.abpsln.
One-time setup¶
After cloning, run the hook installer once:
.githooks/install.ps1 does four things:
- Sets
git config core.hooksPath .githooksso the repo's own hooks are active. - Writes worktree-exclusion entries to
.git/info/exclude(not.gitignore— this is load-bearing; see Git worktrees). - Runs
dotnet tool restoreto install the local .NET tools (CSharpier + Husky.Net) from.config/dotnet-tools.json. - Runs
npm iinfrontend/(installs Prettier and the rest of the npm workspace).
Two more steps are needed before the app will run end-to-end:
The database is created/seeded by Cargonerds.DbMigrator. You normally don't run it by hand —
the Aspire AppHost runs the migrator for you on startup. See
Migrations.
What dotnet tool restore installs
The local tool manifest .config/dotnet-tools.json pins exactly two tools:
csharpier 1.2.6 (with "rollForward": false, so the version is reproducible) and
husky 0.7.2. They are restored into the repo, not installed globally, so every clone
formats identically.
Branching & commits¶
- Branch off
main.mainis the default branch and the base for most PRs. - Opening a PR against
main(ordevelop) triggers.github/workflows/pr-validation.yml, which restores, buildsCargonerds.All.slnin Release, and runs the formatting checks (see CI parity). - Pushing a feature branch deploys it to an ephemeral Azure Container Apps environment
named after the (sanitised) branch.
main,productionandstagingare protected branches (PROTECTED_BRANCHES="main|production|staging"in the deploy workflow) and are never auto-torn-down. Everything else gets cleaned up by.github/workflows/teardown.yml. See Deployment Overview.
Commits are reformatted for you
You do not need to run a formatter before committing — the pre-commit hook does it and
re-stages the result, so the committed bytes are already formatted. The catch: the hook
only runs from a client that uses core.hooksPath (set by install.ps1). See the next
section for how it behaves in GUI git clients.
Code formatting (CSharpier + Prettier)¶
This repo enforces formatting through a Husky.Net pre-commit hook. Two formatters run:
- CSharpier — formats all C# files (
**/*.cs). - Prettier — formats staged
frontend/files (ts,tsx,js,jsx,mjs,cjs,json,css,md).
How the hook is wired¶
The hook entry point is .githooks/pre-commit (a POSIX sh script). It does three things:
# .githooks/pre-commit (abridged)
# 1. GUI git clients (Rider, Fork, etc.) don't always inherit shell PATH — prepend dotnet locations.
PATH="$HOME/.dotnet:$HOME/.dotnet/tools:/usr/local/share/dotnet:...:$PATH"
# 2. Capture the originally-staged files, then run the Husky task group.
STAGED=$(git diff --cached --name-only --diff-filter=ACMR)
dotnet husky run --group pre-commit
# 3. Re-stage everything that was originally staged (picks up csharpier + prettier mutations).
echo "$STAGED" | while IFS= read -r f; do [ -n "$f" ] && git add -- "$f"; done
dotnet husky run --group pre-commit reads .githooks/task-runner.json, which defines three
tasks in the pre-commit group:
| Task | Command | Runs against |
|---|---|---|
csharpier |
dotnet csharpier format . |
**/*.cs (whole solution) |
prettier |
node node_modules/prettier/bin/prettier.cjs --write ${staged} (cwd frontend) |
staged frontend/** of the listed extensions |
guard-node-version |
node .githooks/validate-node-version.mjs |
runs when frontend/realtime/Dockerfile, its package.json, or pr-validation.yml is staged |
flowchart TD
A[git commit] --> B[".githooks/pre-commit (sh)"]
B --> C[Prepend dotnet to PATH]
C --> D[Record staged file list]
D --> E["dotnet husky run --group pre-commit"]
E --> F["csharpier: dotnet csharpier format ."]
E --> G["prettier --write \${staged}"]
E --> H["guard-node-version (conditional)"]
F --> I[Re-stage originally staged files]
G --> I
H --> I
I --> J[Commit created with formatted bytes]
CSharpier configuration¶
CSharpier reads two files at the repo root:
-
.csharpierrc.yaml— the style: -
.csharpierignore— paths CSharpier skips, so scaffolded/non-code files are left alone:
Running the formatters manually¶
dotnet csharpier format . # format all C#
cd frontend && npx prettier --write . # format all frontend files
GUI git clients and PATH
GUI clients (Rider, Fork, GitHub Desktop, …) often launch hooks without your shell PATH,
so dotnet may not be found. .githooks/pre-commit prepends the common .NET install
locations to work around this and aborts with a clear message if dotnet is still missing.
If your SDK lives somewhere unusual, add that path to the PATH= line in
.githooks/pre-commit.
CSharpier honours .gitignore — which is why worktrees go in .git/info/exclude
CSharpier skips anything matched by .gitignore. This single fact is the reason for the
worktree rule below: a worktree under a .gitignore-d path would be invisible to CSharpier,
so unformatted C# could slip through and break CI. Keep reading.
Git worktrees (mandatory rule)¶
When you (or an AI agent) create a git worktree, place it only under one of:
.claude/worktrees/<name>— preferred (Claude Code's default).worktrees/<name>
Do not create worktrees anywhere else, and never add a worktree path to .gitignore.
This is documented for agents in AGENTS.md (referenced from CLAUDE.md).
Why it is mandatory, not a preference¶
These directories are excluded from git per-clone via .git/info/exclude, written by
.githooks/install.ps1:
# .githooks/install.ps1 (abridged)
$gitCommonDir = (git rev-parse --git-common-dir).Trim()
$excludeFile = Join-Path $gitCommonDir "info/exclude"
foreach ($entry in @("/.claude", ".worktrees")) {
if ($existing -notcontains $entry) { Add-Content -Path $excludeFile -Value $entry }
}
The distinction between .git/info/exclude and .gitignore is the whole point:
| Tracked by git? | Seen by CSharpier? | |
|---|---|---|
Worktree under .claude/worktrees/ or .worktrees/ (excluded via .git/info/exclude) |
No (stays untracked) | Yes — C# inside it still gets formatted |
Worktree under a path you added to .gitignore |
No | No — CSharpier goes blind |
Because CSharpier honours .gitignore but ignores .git/info/exclude, a worktree in the
right place is invisible to git and visible to CSharpier. Put a worktree elsewhere and one of
two things breaks:
- Someone adds its path to
.gitignore→ CSharpier reportsFormatted 0 files, unformatted C# is committed, and CI'sdotnet csharpier check .fails. - The path is neither ignored nor excluded → it pollutes
git statusand invites accidental commits of working-copy files.
Never .gitignore a worktree
If you add a brand-new worktree root, add it to .git/info/exclude and to the list in
.githooks/install.ps1 — never to .gitignore.
Building¶
The repo ships several solution files describing different slices of the same projects (see Solution Structure for the full breakdown):
| Solution | Scope | Typical use |
|---|---|---|
Cargonerds.sln |
main app only (src/ + test/ + Aspire) |
day-to-day app work in an IDE |
Cargonerds.All.sln / Cargonerds.All.slnx |
everything incl. Hub + Pricing modules | CI and full test runs |
modules/hub/Hub.sln |
Hub module only | isolated Hub work |
modules/pricing/Pricing.sln |
Pricing module only | isolated Pricing work |
# restore + build the full solution the way CI does
dotnet restore Cargonerds.All.sln
dotnet build Cargonerds.All.sln --configuration Release
No version numbers on <PackageReference>
The repo uses Central Package Management: versions live in Directory.Packages.props, and
the ABP/Aspire/LeptonX version variables ($(AbpVersion), etc.) live in common.props.
Bumping ABP is a one-line edit to AbpVersion in common.props. Don't add Version="…" to a
PackageReference.
Running locally¶
The day-to-day inner loop:
-
Start everything with Aspire:
The AppHost provisions SQL Server / Redis / RabbitMQ as containers, runs
db-migrator, then startsauth,api,admin(Blazor) and therealtime(Next.js) frontend, and opens the Aspire dashboard. It fails fast with a clear message if Docker isn't running. See Running Locally for the dashboard, run-mode ports and the Azure-Spark-DB option. -
Make your changes.
- Run the relevant tests (
dotnet test, ornpm run testinfrontend/realtime). See Testing. - Commit — the pre-commit hook formats your code automatically.
Working with ABP Studio¶
The solution is fully described to ABP Studio (and the ABP CLI), so you can manage it from Studio's UI as well as the command line:
Cargonerds.abpsln— the ABP Studio solution descriptor: module list, an AspirerunProfile, ak8sProfile, Helm/Kubernetes commands, and the originalcreateCommand(abp new Cargonerds -t app --tiered --ui-framework blazor …).Cargonerds.abpmdl(andmodules/hub/Hub.abpmdl,modules/pricing/Pricing.abpmdl) — per-package module maps that tell Studio which.csprojbelongs to which ABP package.etc/abp-studio/run-profiles/Default.abprun.json— the run profile listing each host project, its launch URL andexecution.order(so Studio starts them in the right sequence).etc/abp-studio/k8s-profiles/local.abpk8s.json— the local Kubernetes profile.- The host projects (
Cargonerds.AuthServer,Cargonerds.HttpApi.Host,Cargonerds.Web.Public) reference theVolo.Abp.Studio.Client.AspNetCorepackage so they integrate with Studio's monitoring while running.
ABP Studio analysis mode
Several modules guard Redis/SQL-touching configuration behind
AbpStudioAnalyzeHelper.IsInAnalyzeMode (e.g. CargonerdsEntityFrameworkCoreModule,
CargonerdsHttpApiHostModule, CargonerdsAuthServerModule, CargonerdsWebPublicModule). This
lets ABP Studio statically analyze the solution without connecting to live infrastructure. If
you add config that opens a Redis/SQL connection at module-config time, add the same guard or
Studio analysis will try to connect and fail.
ABP Studio is also the GUI front-end for the abp CLI used in Code Generation
(API/proxy generation). For reading framework internals, abpSrc/abp-get-all-src.ps1 fetches the
ABP/LeptonX source locally via abp get-source; that tree is git-ignored and is not referenced
by any project.
Running tests¶
The integration tests boot the real ABP application against real infrastructure started in Docker via Testcontainers (SQL Server, Redis, RabbitMQ, Azurite) — so a running Docker daemon is required.
# whole suite, the way CI runs it
dotnet test "Cargonerds.All.sln" --configuration Release --no-build --nologo -- --ignore-exit-code 8
# a single test project
dotnet test test/Cargonerds.Application.Tests
# filter by test name
dotnet test --filter "FullyQualifiedName~SampleRepositoryTests"
Why -- --ignore-exit-code 8
The test projects use xunit.v3 with Microsoft.Testing.Platform
(TestingPlatformDotnetTestSupport=true in common.props), so each builds a self-contained
test executable. Arguments after -- go to that executable. Cargonerds.All.sln contains
projects with no tests (and a console helper app); MTP returns exit code 8 ("no tests ran")
for those, so --ignore-exit-code 8 keeps the overall run green. Don't remove it blindly.
See Testing for the full harness (DockerFixture, SharedTestContainers, the
SparkEnvironment("local") trick, Respawn, and what is mocked vs. run for real).
Database migrations¶
Cargonerds.DbMigrator is the startup project for EF Core tooling. Add a migration with:
dotnet ef migrations add MyMigration \
-p src/Cargonerds.EntityFrameworkCore \
-s src/Cargonerds.DbMigrator \
-c CargonerdsDbContext
Then run the migrator (or restart the AppHost, which runs it for you). CargonerdsDbContext is the
"Default" database; the Hub and Pricing modules have their own contexts and connection strings.
See Migrations and Entity Framework.
Versioning¶
The app version is held in the root common.props (<Version>, currently 3.1.4.0). CI bumps it
via .scripts/UpdateVersion.ps1, producing the chore: bump version to … commits — you normally
don't edit it by hand.
Module versions are independent
modules/hub/common.props and modules/pricing/common.props carry their own
<Version>1.0.0</Version>, separate from the app's 3.1.4.0. Only the root common.props
<Version> is what UpdateVersion.ps1 bumps.
SPARK environment selection¶
Which Hub database the app talks to is chosen by the SPARK_ENVIRONMENT variable
(dev / test / prod / local), which picks the matching appsettings.spark.<env>.json. For
local development against your own Hub DB, create the git-ignored override
src/Cargonerds.HttpApi.Host/appsettings.spark.local.user.json (see the README "Local Development
Setup" and Configuration).
CI parity: what the pipeline checks¶
.github/workflows/pr-validation.yml runs on pull requests to main and develop. To match it
locally before pushing:
| CI step | Command | Local equivalent |
|---|---|---|
| Restore | dotnet restore Cargonerds.All.sln |
same |
| Build | dotnet build Cargonerds.All.sln --no-restore --configuration Release |
same |
| Restore tools | dotnet tool restore |
done by install.ps1; re-run if the manifest changed |
| Check C# formatting | dotnet csharpier check . |
dotnet csharpier format . (then commit) |
| Check frontend formatting | npx prettier --check . (in frontend/) |
cd frontend && npx prettier --write . |
check vs format
CI runs CSharpier/Prettier in check mode (fails on any diff, changes nothing). The
pre-commit hook runs them in write mode. If you commit through the hook, the check passes;
the only way to fail it is to bypass the hook (e.g. git commit --no-verify or a client that
isn't using core.hooksPath).
Related pages¶
- Solution Structure — projects,
common.props, Central Package Management, the solution files. - Prerequisites and Running Locally — environment setup and the Aspire run.
- Code Generation — Nextended.CodeGen DTOs and ABP/EF Core proxy generation.
- Testing and Debugging.
- Migrations and CI/CD.