Goal
Code style, formatting and the ordering of types/type members are enforced and fixed automatically, so
that none of the three needs a human decision or a review comment again.
Scope is the whole repository: src/, tests/ and benchmarks/. Previously only src/ had an analyzer
gate, and ordering was not enforced anywhere.
Decisions
- C# keywords, not BCL type names —
string, int, bool, nint, not String, Int32,
Boolean, IntPtr. Reverses the previous rule; 217 of 283 .cs files.
this. prefix stays, now without exception. No _camelCase for fields. A primary constructor
parameter is assigned to a private readonly backing field and read through this.field: a captured
parameter compiles to a field with no readonly, and C# offers no other way to keep the guarantee.
Nothing enforces this — it is on the author.
- Expression-bodied members wherever possible (already the rule, stays).
- Primary constructors wherever possible.
.editorconfig previously asked for them via
csharp_style_prefer_primary_constructors while disabling IDE0290, the same rule. 6 sites.
- Member order is StyleCop's (SA1201/1202/1203/1204/1214), plus alphabetical sorting inside each
group.
- Violations break the build, not only CI — all three concerns.
- Branch names follow Conventional Branch.
Architecture — three concerns, three tools, no overlap
| Concern |
Tool |
Config |
| Formatting (whitespace, line breaks, wrapping) |
CSharpier 1.3.0 |
.editorconfig, .csharpierignore |
| Code style (semantic) |
Roslyn analyzers, Roslynator |
.editorconfig |
| Ordering — fixing |
ReSharper file layout |
DbConnectionPlus.slnx.DotSettings |
| Ordering — checking |
NewStyleCop.Analyzers 1.2.1 |
stylecop.json |
Each tool owns its concern completely and is switched off inside the others'. No .csharpierrc: the print
width comes from max_line_length in .editorconfig, so the line width has one source of truth.
Why the NewStyleCop.Analyzers fork: upstream's last release is 1.2.0-beta.556 from December 2023
and predates the C# this repo writes. Same diagnostic IDs and same stylecop.json, so switching back is a
one-line change — which is what makes the single-maintainer risk acceptable.
Neither package can fix ordering: in both, ElementOrderCodeFixProvider is [NoCodeFix] and never
registered. StyleCop detects, ReSharper fixes. That split is deliberate, not a workaround.
Where each tool runs
| When |
Runs |
How |
Agent edits a .cs file (PostToolUse hook) |
format |
scripts/tidy-cs.ps1, default scope, ~1s |
| Rider save / Code Cleanup |
format / reorder |
CSharpier plugin; ReorderMembers profile |
tidy-cs.ps1 → -Scope style → -Scope all |
format → + style → + reorder |
CSharpier; dotnet format; jb cleanupcode |
preflight.ps1 |
all three |
tidy-cs.ps1 -Scope all |
| Build (all projects) |
all three |
analyzers + NewStyleCop; CSharpier.MsBuild in check mode |
| CI lint job |
verify all three |
tidy-cs.ps1 -Scope all -Check |
CI does not run three separate checks. Neither cleanupcode nor CSharpier is idempotent alone —
cleanupcode re-indents raw string literals and CSharpier puts them back — so asking either in isolation
always answers "changed". -Check -Scope all tidies the checkout for real and compares a hash of the tree
before against after; the checkout is thrown away at the end of the job.
Work items — all done
1. .editorconfig. Grouped and commented; [*] section plus sections for
[*.{csproj,props,targets,DotSettings}] (tabs), [*.{slnx,config,xml,json,yml,yaml}], [*.md]; BOM
removed; dotnet_style_predefined_type_for_* and dotnet_style_qualification_for_* → true:error;
IDE0290 suppression removed; IDE0055 and RCS1037 → none (CSharpier owns whitespace). No
end_of_line anywhere — .gitattributes owns line endings.
StyleCop is default-deny rather than disabled by ranges: all eight categories off, then the wanted rules
back on. SA1101 is not used — this. comes from the four Roslyn dotnet_style_qualification_* rules.
2. Analyzers for tests and benchmarks. EnforceCodeStyleInBuild and TreatWarningsAsErrors moved to
the root Directory.Build.props. The CA rules stay in src/ only: CA1707 alone fires 2100 times on the
Method_ShouldDoSomething naming the test suite is built around.
3. CSharpier. Tool in .config/dotnet-tools.json, .csharpierignore, and CSharpier.MsBuild in
check mode — an unformatted file fails the build, and the build never rewrites files. The tool and the
MsBuild package versions must stay in step or they disagree.
4. Ordering. DotSettings renamed to match the .slnx; StyleCop file layout with alphabetical
sorting; reorder-only cleanup profile; JetBrains.ReSharper.GlobalTools; NewStyleCop.Analyzers with a
matching stylecop.json.
5. Branch naming. Conventional Branch in CONTRIBUTING.md and AGENTS.md.
6. Documentation and scripts. format-cs.ps1 → tidy-cs.ps1, with -Check and a -Scope parameter
(format / style / all); both hook wrappers; ordering wired into preflight.ps1; AGENTS.md,
CONTRIBUTING.md and .agents/references/code-style.md rewritten.
7. The mechanical commits, kept separate, full unit suite after each:
| Commit |
Tool |
Files |
String → string and friends |
dotnet format style (IDE0049) |
207 |
| Convert to primary constructors |
dotnet format style (IDE0290) |
6 |
| CSharpier reformat |
csharpier format . |
230 of 283 |
| Reorder members |
jb cleanupcode |
127 |
| Satisfy the remaining gates |
dotnet format, jb cleanupcode |
41 |
.git-blame-ignore-revs lists all five. PublicAPI.*.txt: checked, no churn.
8. Gaps nothing was watching. Found while building the above, all now build errors:
- Unused using directives. 20 of them.
IDE0005 is never reported at build time unless the
compilation produces an XML documentation file (roslyn#41640),
so GenerateDocumentationFile is on for every project. CS1591/CS1574 are switched off for tests and
benchmarks, which do not document their members.
- Target-typed
new. 14 sites. IDE0090 only fires where the type is written next to the new,
which the var rule makes impossible here; RCS1250 reaches return, argument and assignment
positions.
- Using order.
SA1208/1209/1210/1211/1217. These check rather than fix — CSharpier already
sorts usings — and report zero violations, which is what says the two tools agree.
What emerged during the work
- Explicit interface implementations vs SA1202. The layout needs one entry per kind (property,
indexer, method) matching ImplementsInterface and Access Is="Private". Without the access test it
also catches implicit implementations and drags Equals(T) away from Equals(object). Events must
not get an entry: StyleCop counts an explicit event as private, and giving it one breaks the build.
- CSharpier contradicts SA1216. It sorts
using static by namespace, interleaved; SA1216 wants them
grouped at the end. CSharpier runs last and always has the final say, so SA1216 is unsatisfiable here and
is deliberately not enabled.
- Overload order is not defined. Same-named overloads tie on every sort key and ReSharper's sort is
stable, so it leaves them where it found them. Both orders are correct; a stable sort leaves the
committed order alone, so CI does not flap.
- IDE0049 has three blind spots, none of which produce a diagnostic:
nint/nuint are not on its
list, it never looks inside nameof(...), and tests/package-consumption/ is not in the solution.
- Primary constructor parameters lose
readonly — the decision above. Found by comparing compiled
output, not by any test.
.git-blame-ignore-revs is fragile. git never warns about a SHA it cannot resolve; it skips the
entry and blame silently goes back to pointing at the tool. A rebase-merge or squash-merge rewrites every
SHA. The CI lint job now fails if any entry stops resolving, and this branch must be merged with a
merge commit.
Risks
Field initializer order. Checked: no behaviour change, by comparing a token multiset of every
.cs file before and after and by inspecting the emitted fields.
jb cleanupcode and .slnx profile lookup (RSRP-502346). Verified working on 2026.2.1.
- Alphabetical order inside a group is not checked by StyleCop — its
elementOrder knows only kind,
accessibility, const, static and readonly. A member in the wrong alphabetical position is silently fixed
by the tidy script, by Rider and by the CI lint job, but does not break the build. Accepted.
- NewStyleCop.Analyzers has one maintainer. Mitigated by identical rule IDs and config.
- Two tools on save in Rider can fight. On save = formatting only; reordering is deliberate.
cleanupcode is slow — it loads the whole solution. It belongs in the full tidy run, in preflight
and in CI, never in a per-edit hook.
CSharpier.MsBuild has no MSBuild inputs/outputs, so it runs on every build. About four seconds on
a full solution build. -p:CSharpier_Bypass=true skips it.
Verification
The mechanical commits come in prepare/apply pairs, so each can be re-run from its parent and compared.
All five were, and these are the measured results:
| Apply commit |
Tool |
Difference |
| C# keywords |
dotnet format style --diagnostics IDE0049 |
none |
| primary constructors |
dotnet format style --diagnostics IDE0290 |
+ 23 stale doc lines |
| CSharpier reformat |
csharpier format . |
none |
| reorder members |
jb cleanupcode + csharpier |
4 files, 10 lines |
| satisfy the gates |
dotnet format + cleanupcode + csharpier |
32 files |
The reorder's 10 lines are Equals/CompareTo overload pairs, for the reason above; its check is instead
that the branch tip is a fixed point, tidy-cs.ps1 -Scope all -Check exiting 0. The last commit is mixed:
its 32 files are the hand edits no tool reaches — the keyword conversions under
tests/package-consumption/ and inside nameof(...), the MySqlEntityManipulator conversion, two
global using lines the SDK already generates, and a set of comments and doc-wording fixes.
Release build: 0 warnings, 0 errors. 3475 unit tests green. No public API change.
Goal
Code style, formatting and the ordering of types/type members are enforced and fixed automatically, so
that none of the three needs a human decision or a review comment again.
Scope is the whole repository:
src/,tests/andbenchmarks/. Previously onlysrc/had an analyzergate, and ordering was not enforced anywhere.
Decisions
string,int,bool,nint, notString,Int32,Boolean,IntPtr. Reverses the previous rule; 217 of 283.csfiles.this.prefix stays, now without exception. No_camelCasefor fields. A primary constructorparameter is assigned to a
private readonlybacking field and read throughthis.field: a capturedparameter compiles to a field with no
readonly, and C# offers no other way to keep the guarantee.Nothing enforces this — it is on the author.
.editorconfigpreviously asked for them viacsharp_style_prefer_primary_constructorswhile disablingIDE0290, the same rule. 6 sites.group.
Architecture — three concerns, three tools, no overlap
.editorconfig,.csharpierignore.editorconfigDbConnectionPlus.slnx.DotSettingsstylecop.jsonEach tool owns its concern completely and is switched off inside the others'. No
.csharpierrc: the printwidth comes from
max_line_lengthin.editorconfig, so the line width has one source of truth.Why the
NewStyleCop.Analyzersfork: upstream's last release is1.2.0-beta.556from December 2023and predates the C# this repo writes. Same diagnostic IDs and same
stylecop.json, so switching back is aone-line change — which is what makes the single-maintainer risk acceptable.
Neither package can fix ordering: in both,
ElementOrderCodeFixProvideris[NoCodeFix]and neverregistered. StyleCop detects, ReSharper fixes. That split is deliberate, not a workaround.
Where each tool runs
.csfile (PostToolUse hook)scripts/tidy-cs.ps1, default scope, ~1sReorderMembersprofiletidy-cs.ps1→-Scope style→-Scope alldotnet format;jb cleanupcodepreflight.ps1tidy-cs.ps1 -Scope allCSharpier.MsBuildin check modetidy-cs.ps1 -Scope all -CheckCI does not run three separate checks. Neither
cleanupcodenor CSharpier is idempotent alone —cleanupcodere-indents raw string literals and CSharpier puts them back — so asking either in isolationalways answers "changed".
-Check -Scope alltidies the checkout for real and compares a hash of the treebefore against after; the checkout is thrown away at the end of the job.
Work items — all done
1.
.editorconfig. Grouped and commented;[*]section plus sections for[*.{csproj,props,targets,DotSettings}](tabs),[*.{slnx,config,xml,json,yml,yaml}],[*.md]; BOMremoved;
dotnet_style_predefined_type_for_*anddotnet_style_qualification_for_*→true:error;IDE0290suppression removed;IDE0055andRCS1037→none(CSharpier owns whitespace). Noend_of_lineanywhere —.gitattributesowns line endings.StyleCop is default-deny rather than disabled by ranges: all eight categories off, then the wanted rules
back on. SA1101 is not used —
this.comes from the four Roslyndotnet_style_qualification_*rules.2. Analyzers for tests and benchmarks.
EnforceCodeStyleInBuildandTreatWarningsAsErrorsmoved tothe root
Directory.Build.props. The CA rules stay insrc/only:CA1707alone fires 2100 times on theMethod_ShouldDoSomethingnaming the test suite is built around.3. CSharpier. Tool in
.config/dotnet-tools.json,.csharpierignore, andCSharpier.MsBuildincheck mode — an unformatted file fails the build, and the build never rewrites files. The tool and the
MsBuild package versions must stay in step or they disagree.
4. Ordering.
DotSettingsrenamed to match the.slnx; StyleCop file layout with alphabeticalsorting; reorder-only cleanup profile;
JetBrains.ReSharper.GlobalTools;NewStyleCop.Analyzerswith amatching
stylecop.json.5. Branch naming. Conventional Branch in
CONTRIBUTING.mdandAGENTS.md.6. Documentation and scripts.
format-cs.ps1→tidy-cs.ps1, with-Checkand a-Scopeparameter(
format/style/all); both hook wrappers; ordering wired intopreflight.ps1;AGENTS.md,CONTRIBUTING.mdand.agents/references/code-style.mdrewritten.7. The mechanical commits, kept separate, full unit suite after each:
String→stringand friendsdotnet format style(IDE0049)dotnet format style(IDE0290)csharpier format .jb cleanupcodedotnet format,jb cleanupcode.git-blame-ignore-revslists all five.PublicAPI.*.txt: checked, no churn.8. Gaps nothing was watching. Found while building the above, all now build errors:
IDE0005is never reported at build time unless thecompilation produces an XML documentation file (roslyn#41640),
so
GenerateDocumentationFileis on for every project.CS1591/CS1574are switched off for tests andbenchmarks, which do not document their members.
new. 14 sites.IDE0090only fires where the type is written next to thenew,which the
varrule makes impossible here;RCS1250reachesreturn, argument and assignmentpositions.
SA1208/1209/1210/1211/1217. These check rather than fix — CSharpier alreadysorts usings — and report zero violations, which is what says the two tools agree.
What emerged during the work
indexer, method) matching
ImplementsInterfaceandAccess Is="Private". Without the access test italso catches implicit implementations and drags
Equals(T)away fromEquals(object). Events mustnot get an entry: StyleCop counts an explicit event as private, and giving it one breaks the build.
using staticby namespace, interleaved; SA1216 wants themgrouped at the end. CSharpier runs last and always has the final say, so SA1216 is unsatisfiable here and
is deliberately not enabled.
stable, so it leaves them where it found them. Both orders are correct; a stable sort leaves the
committed order alone, so CI does not flap.
nint/nuintare not on itslist, it never looks inside
nameof(...), andtests/package-consumption/is not in the solution.readonly— the decision above. Found by comparing compiledoutput, not by any test.
.git-blame-ignore-revsis fragile. git never warns about a SHA it cannot resolve; it skips theentry and blame silently goes back to pointing at the tool. A rebase-merge or squash-merge rewrites every
SHA. The CI lint job now fails if any entry stops resolving, and this branch must be merged with a
merge commit.
Risks
Field initializer order.Checked: no behaviour change, by comparing a token multiset of every.csfile before and after and by inspecting the emitted fields.Verified working on 2026.2.1.jb cleanupcodeand.slnxprofile lookup (RSRP-502346).elementOrderknows only kind,accessibility, const, static and readonly. A member in the wrong alphabetical position is silently fixed
by the tidy script, by Rider and by the CI lint job, but does not break the build. Accepted.
cleanupcodeis slow — it loads the whole solution. It belongs in the full tidy run, in preflightand in CI, never in a per-edit hook.
CSharpier.MsBuildhas no MSBuild inputs/outputs, so it runs on every build. About four seconds ona full solution build.
-p:CSharpier_Bypass=trueskips it.Verification
The mechanical commits come in prepare/apply pairs, so each can be re-run from its parent and compared.
All five were, and these are the measured results:
dotnet format style --diagnostics IDE0049dotnet format style --diagnostics IDE0290csharpier format .jb cleanupcode+csharpierdotnet format+cleanupcode+csharpierThe reorder's 10 lines are
Equals/CompareTooverload pairs, for the reason above; its check is insteadthat the branch tip is a fixed point,
tidy-cs.ps1 -Scope all -Checkexiting 0. The last commit is mixed:its 32 files are the hand edits no tool reaches — the keyword conversions under
tests/package-consumption/and insidenameof(...), theMySqlEntityManipulatorconversion, twoglobal usinglines the SDK already generates, and a set of comments and doc-wording fixes.Release build: 0 warnings, 0 errors. 3475 unit tests green. No public API change.