{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"},{"name":"Jendrik Brack","url":"https://daily-devops.net/authors/jendrik/"}],"description":"Recent content in Roslyn Analyzers: Code Quality \u0026 Enforcement on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/analyzers/feed.json","home_page_url":"https://daily-devops.net/tags/analyzers/","icon":"https://daily-devops.net/images/logo_hu_5926de77762241ba.png","items":[{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eThe third solution is always the one that breaks you.\u003c/p\u003e\n\u003cp\u003eThe first one had a \u003ccode\u003eDirectory.Build.props\u003c/code\u003e somebody hand-rolled on a Friday. The second copied it because copying is faster than thinking. By the third, the props file has diverged in three small ways across the three repositories, the analyzer ruleset is somewhere between \u0026ldquo;off\u0026rdquo; and \u0026ldquo;on but quietly ignored\u0026rdquo;, the NuGet metadata is half-filled because nobody remembered which fields were required, and the \u003ccode\u003eLangVersion\u003c/code\u003e is whatever the most recent dev set when they got tired of waiting for the team to decide.\u003c/p\u003e\n\u003cp\u003eNobody is wrong. Everybody is inconsistent.\u003c/p\u003e\n\u003cp\u003eThe standard answer is \u0026ldquo;we\u0026rsquo;ll add a Directory.Build.props to every repo.\u0026rdquo; It works, until it doesn\u0026rsquo;t, because there is no upgrade path. Tighten a rule centrally and you\u0026rsquo;ve got a manual chore to apply it across N repos, in N pull requests, with N reviewers who have other things to do. Templates (\u003ccode\u003edotnet new\u003c/code\u003e) get this even more wrong: they snapshot defaults at fork time and then never improve.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/dailydevops/defaults\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eNetEvolve.Defaults\u003c/code\u003e\u003c/a\u003e is what happens when you stop accepting that trade-off and ship your build defaults as \u003cstrong\u003ea versioned NuGet package\u003c/strong\u003e. Every repo references it like any other dependency. Bumping the version is one PR per repo, fully tooled by Renovate or Dependabot, and the entire surface (MSBuild properties, targets, \u003ccode\u003e.editorconfig\u003c/code\u003e, analyzer rules, NuGet quality enforcement, GitVersion configuration) upgrades together. This article walks through what\u0026rsquo;s in the box, why a NuGet package is the right delivery vehicle, and where the approach stops scaling.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-drift-problem-has-a-name\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#the-drift-problem-has-a-name\" title=\"The Drift Problem Has a Name\"\u003eThe Drift Problem Has a Name\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLook at three .NET solutions that have been alive for more than a year. Solution A pinned \u003ccode\u003eLangVersion=12\u003c/code\u003e, has nullable on, ships no analyzers because \u0026ldquo;we\u0026rsquo;ll add them later.\u0026rdquo; Solution B is on \u003ccode\u003eLangVersion=preview\u003c/code\u003e, nullable warnings only, analyzers configured by hand against an older ruleset. Solution C copy-pasted its \u003ccode\u003eDirectory.Build.props\u003c/code\u003e from A two years ago and nobody has touched it since.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s the drift. Not malice, not negligence. The .NET ecosystem accumulates configuration faster than humans naturally synchronize. Every solution becomes a small archaeological site. Auditing the diffs across them is an afternoon nobody schedules.\u003c/p\u003e\n\u003cp\u003eA \u003ccode\u003eDirectory.Build.props\u003c/code\u003e per repo doesn\u0026rsquo;t fix this: it just \u003cem\u003elocalizes\u003c/em\u003e the drift. The file is still hand-maintained, still copy-pasted, still divergent. You haven\u0026rsquo;t centralized anything; you\u0026rsquo;ve added a layer that hides the divergence one step deeper.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-a-nuget-package-is-the-right-vehicle\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#why-a-nuget-package-is-the-right-vehicle\" title=\"Why a NuGet Package Is the Right Vehicle\"\u003eWhy a NuGet Package Is the Right Vehicle\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eA NuGet package is a versioned, signed, discoverable artifact with built-in upgrade tooling. That\u0026rsquo;s exactly the shape of the problem.\u003c/p\u003e\n\u003cp\u003eReference the package with \u003ccode\u003ePrivateAssets=\u0026quot;all\u0026quot;\u003c/code\u003e and three things happen at build time. NuGet pulls \u003ccode\u003ebuild/NetEvolve.Defaults.props\u003c/code\u003e and injects it into the build before \u003ccode\u003eDirectory.Build.props\u003c/code\u003e, so the defaults set the floor. \u003ccode\u003ebuild/NetEvolve.Defaults.targets\u003c/code\u003e runs after \u003ccode\u003eMicrosoft.Common.targets\u003c/code\u003e, so post-build customizations get applied. The \u003ccode\u003e.editorconfig\u003c/code\u003e ships as build content, so the IDE picks it up without anyone copying a file. Analyzer DLLs register through the standard analyzer-package convention, so diagnostics light up live in the editor.\u003c/p\u003e\n\u003cp\u003eThe whole installation looks like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;ItemGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;PackageReference\u003c/span\u003e \u003cspan class=\"na\"\u003eInclude=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;NetEvolve.Defaults\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;x.x.x\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ePrivateAssets=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;all\u0026#34;\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;PackageReference\u003c/span\u003e \u003cspan class=\"na\"\u003eInclude=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;NetEvolve.Defaults.Analyzer\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;x.x.x\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ePrivateAssets=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;all\u0026#34;\u003c/span\u003e \u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/ItemGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003ePrivateAssets=\u0026quot;all\u0026quot;\u003c/code\u003e is the load-bearing attribute. It tells NuGet \u0026ldquo;build-time only, never propagate this to anyone who consumes my package\u0026rdquo;. You don\u0026rsquo;t want your opinions leaking into other people\u0026rsquo;s projects, and you don\u0026rsquo;t want your downstream consumers locked into your build defaults. The setting is also what makes the analyzer rules quiet when \u003cem\u003eyour\u003c/em\u003e package is being consumed downstream: they only run inside \u003cem\u003eyour\u003c/em\u003e build.\u003c/p\u003e\n\u003cp\u003eThe upgrade story is the part that templates can never match. Bump the version once in \u003ccode\u003eDirectory.Packages.props\u003c/code\u003e, open a PR in each consumer, let Renovate or Dependabot bundle the chore. Tighter analyzer rules, updated \u003ccode\u003e.editorconfig\u003c/code\u003e, new MSBuild properties: they all land in one diff per repo. There is no manual cross-repo diff hunt anymore.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"whats-actually-in-the-box\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#whats-actually-in-the-box\" title=\"What\u0026rsquo;s Actually in the Box\"\u003eWhat\u0026rsquo;s Actually in the Box\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe repository ships two packages with clearly separated responsibilities.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.Defaults\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eNetEvolve.Defaults\u003c/code\u003e\u003c/a\u003e\u003c/strong\u003e is the foundational layer. Its \u003ccode\u003eDirectory.Build.props\u003c/code\u003e declares the package itself as a source-only artifact with description, repository URL, project URL, package tags, and a \u003ccode\u003eCopyrightYearStart\u003c/code\u003e of 2024, the same metadata it then asks every consumer to fill in. Inside the NuGet build folder, props and targets get injected automatically, supporting both single-target and multi-target projects. The package ships template files for \u003ccode\u003e.editorconfig\u003c/code\u003e, \u003ccode\u003e.gitignore\u003c/code\u003e, \u003ccode\u003e.gitattributes\u003c/code\u003e, and \u003ccode\u003e.csharpierignore\u003c/code\u003e under \u003ca href=\"https://github.com/dailydevops/defaults/tree/main/src/NetEvolve.Defaults/configurations\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003esrc/NetEvolve.Defaults/configurations\u003c/code\u003e\u003c/a\u003e, the bits every repo wants identical and nobody enjoys hand-syncing. Modern C# is the baseline: nullable reference types on, implicit usings on, file-scoped namespaces, and the C# 13 language features that ship with .NET 10. NuGet Audit is wired in for vulnerability scanning, and GitVersion handles semantic versioning so the version number falls out of git history instead of being argued about in stand-up.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.Defaults.Analyzer\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eNetEvolve.Defaults.Analyzer\u003c/code\u003e\u003c/a\u003e\u003c/strong\u003e is the enforcement layer. Ten diagnostic rules (NED0001 through NED0010) catch the NuGet package metadata problems that always surface six months too late: when someone realises the package on nuget.org has no description, the repo link is broken, the authors field is missing, and nobody put a license expression in the project file before the legal team noticed. Each rule emits a Roslyn diagnostic with a fix description and a link to a \u003ca href=\"https://github.com/dailydevops/defaults/tree/main/docs/usage\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eusage doc\u003c/a\u003e explaining the \u003cem\u003ewhy\u003c/em\u003e, not just the \u003cem\u003ewhat\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003eThe rule set covers the metadata fields you regret losing later:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNED0001: Missing PackageId.\u003c/strong\u003e Without it the package can\u0026rsquo;t be created or published; consumers can\u0026rsquo;t reference what doesn\u0026rsquo;t exist.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0002: Missing Package Title.\u003c/strong\u003e The human-readable name shown in nuget.org search.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0003: Missing Package Description.\u003c/strong\u003e The blurb that decides whether anyone clicks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0004: Missing Package Tags.\u003c/strong\u003e Discovery.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0005: Missing PackageProjectUrl.\u003c/strong\u003e The \u0026ldquo;where does this thing live\u0026rdquo; link.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0006: Missing Repository URL.\u003c/strong\u003e The \u0026ldquo;where does the source live\u0026rdquo; link, separate from project URL by NuGet convention.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0007: Missing Authors.\u003c/strong\u003e Attribution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0008: Missing Company.\u003c/strong\u003e Organisational ownership for compliance and licensing.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0009: Missing CopyrightYearStart.\u003c/strong\u003e Lets the targets file compute a sensible \u003ccode\u003eCopyright\u003c/code\u003e string instead of last-year-or-this-year hand-coded.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNED0010: Missing Recommended Analyzer Package.\u003c/strong\u003e Build-config rule for \u003cem\u003eall\u003c/em\u003e projects (not just packable ones): surfaces consumers that pulled in \u003ccode\u003eNetEvolve.Defaults\u003c/code\u003e but forgot the matching analyzer.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese are all Warning severity. Real misery comes from finding any one of them missing in a package that\u0026rsquo;s already on nuget.org with three hundred downloads. That\u0026rsquo;s exactly where a build-time analyzer earns its keep.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-customization-still-looks-like\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#what-customization-still-looks-like\" title=\"What Customization Still Looks Like\"\u003eWhat Customization Still Looks Like\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCentralised defaults only work if they remain overridable. The package follows the standard MSBuild override rules: any setting can be tightened, loosened, or replaced in the consuming project\u0026rsquo;s own \u003ccode\u003e.csproj\u003c/code\u003e or \u003ccode\u003eDirectory.Build.props\u003c/code\u003e. Want preview language features in one repo only? Override \u003ccode\u003e\u0026lt;LangVersion\u0026gt;preview\u0026lt;/LangVersion\u0026gt;\u003c/code\u003e. Need to disable nullable for a legacy project the package can\u0026rsquo;t refactor for you? Set \u003ccode\u003e\u0026lt;Nullable\u0026gt;disable\u0026lt;/Nullable\u0026gt;\u003c/code\u003e locally. Targeting a custom framework matrix? \u003ccode\u003e\u0026lt;TargetFrameworks\u0026gt;net8.0;net9.0\u0026lt;/TargetFrameworks\u0026gt;\u003c/code\u003e wins over whatever the package set.\u003c/p\u003e\n\u003cp\u003eThe escape hatches exist precisely so the defaults can be opinionated. A package that tries to be infinitely configurable becomes its own configuration problem. NetEvolve.Defaults stays opinionated and lets you override one property at a time when you really need to.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-the-approach-wins\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#where-the-approach-wins\" title=\"Where the Approach Wins\"\u003eWhere the Approach Wins\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe biggest win isn\u0026rsquo;t initial setup. It\u0026rsquo;s the \u003cem\u003enext\u003c/em\u003e tightening pass.\u003c/p\u003e\n\u003cp\u003eA centralised analyzer rule that catches missing repository URLs on every package the org ships, applied across thirty repositories, by bumping a single version in \u003ccode\u003eDirectory.Packages.props\u003c/code\u003e. That is the kind of leverage hand-edited build files cannot match.\u003c/p\u003e\n\u003cp\u003eOther concrete wins compound from there. CI/CD environments get auto-detected and optimised: the package recognises a CI build and tunes accordingly, so you don\u0026rsquo;t have to remember which \u003ccode\u003e\u0026lt;ContinuousIntegrationBuild\u0026gt;true\u0026lt;/ContinuousIntegrationBuild\u0026gt;\u003c/code\u003e flag to set where. NuGet Audit runs as part of the default build, so vulnerability scanning is \u003cem\u003eon by default\u003c/em\u003e rather than opt-in by remembering. GitVersion produces a deterministic version number from git history and tags, ending per-repo bikeshedding about whether the next bump is \u003ccode\u003e1.4.0\u003c/code\u003e or \u003ccode\u003e1.3.6\u003c/code\u003e. And the \u003ccode\u003e.editorconfig\u003c/code\u003e baseline lives in one place, applied to every repo, evolved through the same upgrade flow as the props and targets.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-it-stops-scaling\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#where-it-stops-scaling\" title=\"Where It Stops Scaling\"\u003eWhere It Stops Scaling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe honest pushback: this is \u003cem\u003eopinionated\u003c/em\u003e. It encodes one organisation\u0026rsquo;s view of how a .NET project should be built, packaged, and versioned. Pull it into a team with different opinions (manual semantic versioning instead of GitVersion, looser analyzer policies, different package-metadata expectations) and you spend more time disabling things than you save by adopting it.\u003c/p\u003e\n\u003cp\u003eGitVersion is the sharpest coupling. If you run a manual-tag versioning shop, NetEvolve.Defaults\u0026rsquo; assumption that GitVersion drives the version number fights you. Analyzer noise on legacy repos is the second friction point: dropping ten new diagnostics onto a brownfield codebase will produce dozens of warnings, and you\u0026rsquo;ll need a controlled \u003ccode\u003e\u0026lt;NoWarn\u0026gt;\u003c/code\u003e escape hatch, or a phased adoption where you fix the diagnostics in waves rather than all at once.\u003c/p\u003e\n\u003cp\u003eThe third caveat is reach. A defaults package works inside one organisation\u0026rsquo;s value system. It does not translate cleanly across organisations with different conventions. That\u0026rsquo;s fine. The point isn\u0026rsquo;t to write \u003cem\u003ethe\u003c/em\u003e defaults package; it\u0026rsquo;s to write \u003cem\u003eyour\u003c/em\u003e defaults package and treat it as production infrastructure.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-to-roll-your-own\"\u003e\u003ca href=\"/posts/netevolve-defaults-build-automation/#when-to-roll-your-own\" title=\"When to Roll Your Own\"\u003eWhen to Roll Your Own\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe scaling threshold is honestly low. One or two solutions? A hand-maintained \u003ccode\u003eDirectory.Build.props\u003c/code\u003e is enough. Three to ten? Centralised defaults pay back in months. The upgrade-flow leverage alone covers the setup. Ten or more solutions, or distributed teams across multiple time zones? Drift is otherwise terminal, and a defaults package is no longer a nice-to-have.\u003c/p\u003e\n\u003cp\u003eThe principle generalises. Build configuration is production code. It deserves the same treatment: versioned, packaged, tested, upgraded through the same channels as your runtime dependencies. The friction of doing this right is one weekend of work the first time. The friction of \u003cem\u003enot\u003c/em\u003e doing it compounds quarter over quarter for the life of every repository that depends on the drifted version.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eNetEvolve.Defaults\u003c/code\u003e is one concrete answer to that problem. It\u0026rsquo;s not the only possible shape: a private feed, an internal SDK, a Directory.Build.props in a git submodule all solve some part of it. But shipping defaults as a normal NuGet package is the lowest-friction way to get the upgrade flow right, and that flow is the part that quietly determines whether the defaults stay current or rot.\u003c/p\u003e\n\u003cp\u003eStandardise or drift. Pick one before the third solution forces the choice on you.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eBuild configuration is production code. Version it, package it, upgrade it through the same channels as the runtime dependencies it sits next to.\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-06-19T08:46:47+02:00","date_published":"2026-06-19T08:45:00+02:00","id":"https://daily-devops.net/posts/netevolve-defaults-build-automation/","language":"en","summary":"NetEvolve.Defaults centralizes MSBuild defaults, analyzer rules, and .editorconfig in one versioned NuGet package. Upgrade once, propagate everywhere.","tags":["dotnet","msbuild","nuget","analyzers","bestpractices","softwareengineering"],"title":"Standardize or Drift: One Defaults Package for All Your Solutions\n","url":"https://daily-devops.net/posts/netevolve-defaults-build-automation/"}],"language":"en","title":"Roslyn Analyzers: Code Quality \u0026 Enforcement on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}