dotnet-project-hygiene
Skill2 starsChanged 43 days agoMITView on GitHub
Use when setting up or reviewing a .NET repo's build plumbing — Directory.Packages.props / central package management, Directory.Build.props/.targets, solution format, global.json SDK pinning, analyzer/warning settings, .editorconfig, deterministic/CI builds, and lock files. Prefer the repo's existing conventions over introducing new ones.
In this repository56 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- benchmarkingSkill
- commit-messagesSkill
- csharp-15-syntaxSkill
- csharp-aot-trimmingSkill
- csharp-api-designSkill
- csharp-assembly-inspectionSkill
- csharp-asyncSkill
- csharp-collections-modernSkill
- csharp-concurrency-primitivesSkill
- csharp-data-layoutSkill
- csharp-debuggingSkill
- csharp-discardsSkill
- csharp-disposable-patternsSkill
- csharp-docsSkill
- csharp-error-handlingSkill
- csharp-extension-membersSkill
- csharp-generics-modernSkill
- csharp-guard-clausesSkill
- csharp-json-source-generationSkill
- csharp-language-versionsSkill
- csharp-logging-diagnosticsSkill
- csharp-lspSkill
- csharp-modern-typesSkill
- csharp-nullabilitySkill
- csharp-nullable-migrationSkill
- csharp-nunitSkill
- csharp-pattern-matchingSkill
- csharp-performanceSkill
- csharp-polyfillsSkill
- csharp-source-generatorsSkill
- csharp-static-lambdasSkill
- csharp-string-handlingSkill
- csharp-tunitSkill
- csharp-verificationSkill
- dispatching-subagentsSkill
- dotnet-11-librariesSkill
- dotnet-analyzersSkill
- dotnet-central-package-managementSkill
- dotnet-file-based-appsSkill
- dotnet-msbuild-usingsSkill
- dotnet-multi-targetingSkill
- dotnet-runSkill
- dotnet-runtime-asyncSkill
- dotnet-slnxSkill
- dotnet-test-platformsSkill
- executing-plansSkill
- performance-bounded-deduplicationSkill
- roslyn-analyzer-performanceSkill
- roslyn-discoverySkill
- roslyn-duplicate-detectionSkill
- roslyn-guard-rewritersSkill
- roslyn-rewritersSkill
- subagent-driven-developmentSkill
- tddSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 37
If a repo already has scattered per-project versions and no Directory.Packages.props, migrating is a deliberate, repo-wide change — don't …
- Very specific: line 87
Don't add <TargetFrameworks> (plural) with a long multi-TFM list speculatively.
- Very specific: line 103
Don't add a repo-wide <NoWarn> list to make a warning go away.
- Distinctive: line 109
Never suppress in the first instance: Read the warning, find what the code does wrong, and fix it.
- Very specific: line 113
Use the narrowest scope: Put [SuppressMessage("Category", "ID", Justification = "...")] on the one member.
- Very specific: line 146
SourceLink: add the provider package (Microsoft.SourceLink.GitHub, .GitLab, etc.) so consumers stepping into your NuGet package's code get …
- Very specific: line 158
Solution filters: create a .slnf with dotnet new slnf -s App.slnx, then edit it with dotnet sln App.slnf add/remove/list.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
.NET project hygiene
The repo's build plumbing (.props, .targets, .editorconfig, global.json) is infrastructure other people rely on. Match what's already there before adding something new — a second, conflicting way to pin versions or set warnings is worse than the thing it "fixes."
Central package management (CPM)
One version per package, set once, referenced everywhere. Put it in Directory.Packages.props at the repo root:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
</ItemGroup>
</Project>Project files then reference packages without a version:
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>CentralPackageTransitivePinningEnabledmakes aPackageVersionentry also apply when that package arrives transitively, so you can raise a vulnerable transitive dependency centrally. It only affects packages you list, and a pinned package becomes a dependency of any package you pack. Turn it on unless you have a specific reason not to.- For discovery rules,
GlobalPackageReference, pinning behaviour, lock files, source mapping, migration and error codes, seedotnet-central-package-management. VersionOverridelets one project deviate from the central version. Use it sparingly and only with a comment saying why (e.g. a security patch that hasn't landed centrally yet); it defeats the point of CPM if used routinely.- If a repo already has scattered per-project versions and no
Directory.Packages.props, migrating is a deliberate, repo-wide change — don't do it as a drive-by inside an unrelated task.
Directory.Build.props / Directory.Build.targets
MSBuild auto-imports these by walking up from each project file — no explicit <Import> needed.
Directory.Build.props: imported early, before the SDK sets defaults. Put properties here that projects should be able to override (TargetFramework,LangVersion,Nullable, analyzer settings).Directory.Build.targets: imported late, after the project file. Put things here that should win regardless of what an individual project sets, or that depend on items/properties the project defines (custom targets, post-build steps).- One set at the repo root is usually enough. A second, nested
Directory.Build.propsin a subtree (e.g.tests/) is for narrowing scope (test-only packages, relaxed nullable) — know that MSBuild merges them, it doesn't replace the parent, so don't duplicate properties trying to "reset" them.
Solution format: .slnx only
.slnx is the solution format. .sln is legacy: migrate it, don't keep both. .slnx is short, diff-friendly XML with no GUIDs to collide on merge, and dotnet new sln already creates it by default on SDK 10+:
dotnet new sln -n MyRepo
dotnet sln MyRepo.slnx add src/MyProject/MyProject.csproj
dotnet build MyRepo.slnx
dotnet sln MyRepo.sln migrate # convert a legacy .sln, then delete the .sln.slnf solution filters are still valid alongside .slnx, as named subsets of projects. See dotnet-slnx for migration, format details, filters and gotchas.
global.json — pin the SDK, don't wing it
{
"sdk": {
"version": "10.0.401",
"rollForward": "latestFeature"
}
}- Pin to a specific installed SDK version so
dotnet buildis reproducible across machines and CI, instead of whatever floating SDK happens to be first onPATH. rollForwardcontrols what happens when the pinned version isn't installed:latestPatch(default-ish, narrowest — same feature band, higher patch),latestFeature(same major, allows a newer feature band — good for "give me at least this, but let CI have a newer bugfix band"),latestMinor/latestMajor(wider, rarely what you want),disable(exact match only, fails otherwise). Pick the narrowest one that still lets CI machines with slightly newer SDKs build.- A prerelease SDK (
-rc,-preview) installed alongside stable ones is exactly the caseglobal.jsonexists for — without it,dotnetpicks the highest installed SDK, prerelease included, and behavior becomes machine-dependent.
Language version, nullable, implicit usings
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>LangVersiondefaults to the highest version the targeted TFM supports (net10.0→ C# 14) — set it explicitly only when you need to pin below that, orlatest/previewto opt into a newer compiler than the TFM implies.Nullable enableis the default for new templates and should be the default for you too; a codebase with it off is a codebase with a known, tracked debt, not a reason to leave new code unannotated.- Don't add
<TargetFrameworks>(plural) with a long multi-TFM list speculatively. Every extra TFM multiplies build/test time and#ifcomplexity. Support what you ship, not what you might ship.
Analyzers and warnings
<PropertyGroup>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>AnalysisLevelpicks the analyzer rule vintage (latesttracks the SDK you're on);AnalysisModepicks the default severity bucket (Minimum,Recommended,All) for rules that don't have an explicit.editorconfigentry.EnforceCodeStyleInBuildmakes.editorconfigstyle rules (IDE0xxx) fail the build, not just show up as IDE squiggles — without it, style rules are cosmetic and silently rot.TreatWarningsAsErrorsplus the two settings above is what turns "the analyzers said something" into "the build won't produce a broken artifact." Seecsharp-verificationfor why a warning-emitting build is not a done build.- Don't add a repo-wide
<NoWarn>list to make a warning go away. See "Suppressions are a last resort" below.
Suppressions are a last resort
A warning is a finding about the code. Fix the code first. Most analyzer warnings point at a real bug, and the fix is usually small.
- Never suppress in the first instance. Read the warning, find what the code does wrong, and fix it.
- Never suppress because the fix is hard or tedious. Giving up and suppressing is not done. Keep working the fix.
- Never suppress to make a build or gate pass. A suppression is not a way to finish a task.
- Suppress only when you have tried the fix and the warning is wrong for this code. Examples: a confirmed false positive, a public API shape you cannot change, interop with a signature you do not own.
- Use the narrowest scope. Put
[SuppressMessage("Category", "ID", Justification = "...")]on the one member. Never a file-wide or project-wide suppression for a local problem. - Write the real reason in
Justification. Say why the warning is wrong here. "Not needed" or "false positive" with no detail is not a reason. - Report every suppression you add to the user. Name the rule, the member and the reason.
.editorconfig
Rule severities live per-rule under [*.cs] sections:
[*.cs]
dotnet_diagnostic.CA1062.severity = warning
dotnet_diagnostic.IDE0060.severity = suggestion.editorconfigis where per-rule severity actually lives;AnalysisModeis only the fallback for rules the file doesn't mention.- Lowering a rule's severity to make a build pass is a suppression, same as
#pragma warning disable. It follows the same last-resort rules above. Seecsharp-verification. - A long, deliberately organized
.editorconfigis not append-only. Sort a new line into the section it belongs to, not onto the end of the file:- Find the right
[glob]section first. A file with sections at both the root and narrow, later scopes (e.g. a trailing[**/tests/**/*.cs]) can silently rescope a rule to tests if you anchor on "the last line that looks similar" instead of checking which[...]block your target line actually sits inside. - Then the right comment-header group (e.g.
# Maintainability,# Naming) and insert in the same order the file already uses. Options belong in the options block, not mixed in amongdotnet_diagnostic.*severity lines. - Test-only relaxations belong in a
tests/-scoped file (e.g.tests/.editorconfig), not a narrow glob bolted onto the root file. Product-code rules stay in the root file, in their proper group. - Verify after editing that the inserted line landed inside the section you intended — an edit that "applied" is not the same as one that landed in the right place.
- Find the right
Deterministic and reproducible builds
<PropertyGroup>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>Deterministic(on by default for SDK-style projects) makes two builds from identical source produce byte-identical output.ContinuousIntegrationBuildstrips machine-specific paths from PDBs and is required for SourceLink to resolve correctly from a released package — set it conditionally so localdotnet buildstill embeds local paths for a debugger.- SourceLink: add the provider package (
Microsoft.SourceLink.GitHub,.GitLab, etc.) so consumers stepping into your NuGet package's code get it fetched from source control instead of a stale local copy. NeedsContinuousIntegrationBuild=trueandEmbedUntrackedSources=trueon the CI build that produces the package.
NuGet audit and lock files
NuGetAudit(on by default in current SDKs) checks restored packages against the NuGet vulnerability feed and warns on known CVEs — leave it on; treatNU1900-series warnings as real findings, not noise to suppress.- Package lock files (
packages.lock.json, via<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>): use them when you need byte-for-byte reproducible restores across machines/CI regardless of what's newly published upstream (e.g. compliance, supply-chain requirements, or flaky transitive updates breaking builds). They're extra ceremony (a file to commit and refresh on every version bump) — don't add them to a repo that hasn't asked for that guarantee; CPM already gives you a single place to control versions.
.NET 11 (net11.0, RC1)
net11.0targeting.net11.0compiles and definesNET11_0_OR_GREATERas expected. .NET 11 is at RC1 with GA expected November 2026 — keepnet10.0the default TFM for anything shipping before then; addnet11.0only where you have a concrete reason (probing new APIs, an explicit multi-target). If a prerelease SDK is installed alongside stable ones, pinglobal.json(see above) sodotnet builddoesn't silently pick the RC.- MSBuild server and the Native AOT CLI path are on by default in SDK 11 — repeated builds reuse persistent
MSBuild/VBCSCompilernode processes without any opt-in property (adotnet buildleavesnodeReuse:trueMSBuild processes running).--disable-build-serversis there for the case you need a truly clean, non-cached build (CI diagnosing a flaky build, or benchmarking cold build time) — reach for it deliberately, not as a default habit. - CA1873 (avoid potentially expensive logging calls — flags computing/formatting log arguments that a guarding
IsEnabledcheck would have skipped) has improved detection in SDK 11's analyzer set. If it fires on code that previously passed, it's very likely a real one — seecsharp-logging-diagnosticsfor theIsEnabledguard pattern it wants. - Solution filters: create a
.slnfwithdotnet new slnf -s App.slnx, then edit it withdotnet sln App.slnf add/remove/list. There is no separate subcommand:dotnet slnaccepts the.slnfas its file argument. Seedotnet-slnx.
Build files: comment only genuinely confusing XML
In .csproj, .props, and .targets, a comment is for a bit of XML whose behavior genuinely isn't obvious — not for narrating what a property does or why it was changed.
- Default to no comment. A named property, a
PackageReference, a TFM list, an ordinaryItemGroupexplains itself; if a reader can tell what it does by reading it, say nothing. - One line, only when the XML really is confusing — evaluation order, a non-obvious MSBuild interaction, a workaround for a specific error code. Never a multi-line block or a rationale essay; background and trade-offs belong outside the build file.
- Comments describe the file as it is, not its history. No "now", "previously", "used to", "no longer", "originally" — those describe a change, not the current XML. Never comment on something that's absent: removing a property or an
ItemGroupleaves nothing to explain, so it earns no comment either.
Don't fight the existing setup
Before changing any of the above, check what the repo already does — the whole point is one consistent, discoverable answer per concern. A new Directory.Build.props property that duplicates one already set at the root, a second CPM-like scheme, or a personal .editorconfig override are all worse than living with the existing convention.
- Don't create or edit repo config as a side effect of an unrelated task.
.editorconfig,global.json,nuget.config, andDirectory.Build.propsseverities shape the whole repo; a failing analyzer or a red CI leg is a reason to fix the code and report the cause, not to add or loosen config to make the symptom go away.
Checklist
- Versions come from one place (
Directory.Packages.props, CPM on); no scattered per-project versions Directory.Build.props/.targetsused for their intended timing (early vs. late), not duplicated per project- Solution file is
.slnx; any legacy.slnmigrated and deleted (seedotnet-slnx) global.jsonpins the SDK with a deliberaterollForward, especially if prerelease SDKs are installedNullable,ImplicitUsingsenabled;LangVersionexplicit only when pinning below the TFM defaultTreatWarningsAsErrors+EnforceCodeStyleInBuild+AnalysisLevel latest— warnings and style rules actually fail the build- Rule severities live in
.editorconfig, not ad hoc#pragma/NoWarn - Every warning was fixed in the code; any suppression is member-scoped, has a real
Justification, and was reported ContinuousIntegrationBuildset for CI/package-producing builds; SourceLink wired for published packagesNuGetAuditleft on; lock files added only when the repo genuinely needs reproducible restores- Changes match the repo's existing conventions instead of introducing a parallel one
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

