{"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 Software Engineering Principles and Practices on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/softwareengineering/feed.json","home_page_url":"https://daily-devops.net/tags/softwareengineering/","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\u003eIn nearly twenty years of writing .NET, I have left more companies than I currently remember the names of.\u003c/p\u003e\n\u003cp\u003eThe companies have, in roughly equal measure, forgotten me. My Slack accounts are deactivated, my email aliases bounce, the wiki pages I wrote have been archived or quietly deleted by someone reorganizing the space. The architectural decisions I argued for in rooms that no longer exist are now policy, or have been reversed, or have become folklore.\u003c/p\u003e\n\u003cp\u003eWhat hasn\u0026rsquo;t forgotten me is the code.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003egit log\u003c/code\u003e still has my name on it. Methods I wrote in 2014 are still running at companies I left years ago. A junior engineer somewhere is, right now, opening a file and seeing \u003ccode\u003eAuthor: Martin Stühmer\u003c/code\u003e next to a line they\u0026rsquo;re trying to understand. They can\u0026rsquo;t ask me anything.\u003c/p\u003e\n\u003cp\u003eThe first four parts of this series (\u003ca href=\"/posts/code-as-legacy/\"\u003eone\u003c/a\u003e, \u003ca href=\"/posts/code-as-legacy-past-self/\"\u003etwo\u003c/a\u003e, \u003ca href=\"/posts/code-as-legacy-empty-promises/\"\u003ethree\u003c/a\u003e, \u003ca href=\"/posts/code-as-legacy-age-of-ai/\"\u003efour\u003c/a\u003e) treated legacy as a relationship between me and a future version of myself. That framing is incomplete. The most consequential version of \u003cem\u003eFuture Self\u003c/em\u003e is often not me at all. It\u0026rsquo;s a stranger, at a company I no longer work for, reading the code I left behind.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-continuity-illusion\"\u003e\u003ca href=\"/posts/code-as-legacy-after-you-leave/#the-continuity-illusion\" title=\"The Continuity Illusion\"\u003eThe Continuity Illusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe earlier parts share a hidden assumption: that you\u0026rsquo;re still around. Past Self left the mess; Future Self cleans it up; both are \u003cem\u003eyou\u003c/em\u003e, separated by time. That\u0026rsquo;s legacy inside one employment. It\u0026rsquo;s the smaller version of the problem.\u003c/p\u003e\n\u003cp\u003eThe larger version: you won\u0026rsquo;t be around. You\u0026rsquo;ll have moved teams, moved companies. Your access is revoked. The Slack thread where you explained the design has aged out of retention. The colleague who could have answered on your behalf left a year after you did.\u003c/p\u003e\n\u003cp\u003eWhat remains is a binary fact: the code, and a stranger, in a room together, with no intermediary.\u003c/p\u003e\n\u003cp\u003eEvery company I\u0026rsquo;ve worked for has had at least one piece of code I wrote that survived three rounds of personnel turnover after I left. The people who reviewed it are gone. The product manager who set the requirements is gone. The reason the method exists is, in many cases, no longer reconstructable from anything except the code itself. Useful code survives: that\u0026rsquo;s the whole point of writing it. Surviving means outlasting the context.\u003c/p\u003e\n\u003cp\u003eI once got a LinkedIn message from someone I\u0026rsquo;d never met, working at a company I\u0026rsquo;d left in 2018, asking what a particular \u003ccode\u003eTimeoutPolicy\u003c/code\u003e was supposed to do. The class had my name on the original commit. I had no memory of writing it, no access to the repository to read the surrounding code, and no useful answer. He had inherited a maintenance question I could no longer help with. Six years and three jobs after I\u0026rsquo;d left it for him. That exchange is the one I think about when I write anything I expect to outlive my involvement.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-the-codebase-remembers\"\u003e\u003ca href=\"/posts/code-as-legacy-after-you-leave/#what-the-codebase-remembers\" title=\"What the Codebase Remembers\"\u003eWhat the Codebase Remembers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe artifacts a company actually preserves across years and reorganizations are a short list:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe source code.\u003c/li\u003e\n\u003cli\u003eThe commit history (until someone rewrites it for \u0026ldquo;cleanliness,\u0026rdquo; which is its own crime).\u003c/li\u003e\n\u003cli\u003eA small number of documents someone deliberately curated.\u003c/li\u003e\n\u003cli\u003eThe data, sometimes.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eSlack ages out. Wiki pages rot. Confluence reorganizations bury entire hierarchies. Tickets get archived when the tracker is replaced.\u003c/p\u003e\n\u003cp\u003eNotice what\u0026rsquo;s not on the list: the \u003cem\u003ewhy\u003c/em\u003e. The why was almost always in the medium that didn\u0026rsquo;t survive: the meeting, the Slack thread, the architect\u0026rsquo;s verbal walkthrough at the offsite, the principal engineer\u0026rsquo;s pre-review feedback in a 1:1 that was never written down.\u003c/p\u003e\n\u003cp\u003eIf you want the \u003cem\u003ewhy\u003c/em\u003e to survive, encode it in something that survives: the structure of the code, comments that explain motivation rather than mechanism, commit messages that say more than \u003cem\u003efix bug\u003c/em\u003e, ADRs versioned in the repository, tests that document expected behavior. All of these live in the repository. The repository is the only artifact with the same survival profile as the code, because it \u003cem\u003eis\u003c/em\u003e the code.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-handoff-that-never-happens\"\u003e\u003ca href=\"/posts/code-as-legacy-after-you-leave/#the-handoff-that-never-happens\" title=\"The Handoff That Never Happens\"\u003eThe Handoff That Never Happens\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThere\u0026rsquo;s a polite fiction that knowledge transfer happens when someone leaves. The departing engineer writes a document, presents it, answers questions for two weeks, the team continues with full understanding.\u003c/p\u003e\n\u003cp\u003eIt does not work that way.\u003c/p\u003e\n\u003cp\u003eThe departing engineer captures perhaps 30% of what they know, because much is unconscious: patterns recognized on sight, constraints internalized so long ago they no longer notice them. The team skims the document, files it. Six months later, when they meet a problem the document would have addressed, nobody remembers it exists. A year later, the document is in an archived space nobody has access to.\u003c/p\u003e\n\u003cp\u003eThe reliable channel is the code itself. Not because it\u0026rsquo;s a good narrative medium, but because it\u0026rsquo;s the only artifact guaranteed to be in front of the next maintainer when they need it. If the line contains its own justification, the justification reaches them. If it lives in a document they don\u0026rsquo;t know about, it doesn\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-i-write-into-the-code-now\"\u003e\u003ca href=\"/posts/code-as-legacy-after-you-leave/#what-i-write-into-the-code-now\" title=\"What I Write Into the Code Now\"\u003eWhat I Write Into the Code Now\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThese aren\u0026rsquo;t best practices in the abstract. They\u0026rsquo;re habits I formed after watching enough code outlive its team.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCommit messages as the durable narrative.\u003c/strong\u003e A commit message is the only writing about a piece of code guaranteed to travel with it forever. Not the PR description (lives in a tool that may not exist in five years). Not the ticket. The commit message. I write them as if the only reader is a stranger trying to understand why the change happened.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e# Bad: tells you what the diff already shows\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eFix timeout handling in report service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e# Better: tells you why this exists\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRaise report timeout to 30s to match legacy ReportService SLA\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThe legacy ReportService can take up to 28s under load; the previous\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e10s timeout caused intermittent failures during EOM batch runs.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e30s is deliberate, not arbitrary. Revisit when ReportService is\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ereplaced (tracked in #1247).\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eArchitecture Decision Records in the repository.\u003c/strong\u003e Not in the wiki. Not in Notion. In \u003ccode\u003edocs/adr/\u003c/code\u003e, versioned with the code, surviving every reorganization. ADRs are letters from the team that existed in 2024 to the team that exists in 2028. The 2028 team won\u0026rsquo;t have access to the meetings. They will have \u003ccode\u003egit checkout\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eComments that explain the constraint, not the mechanism.\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: explains what the next line already says\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003elong\u003c/span\u003e \u003cspan class=\"n\"\u003eNext\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_value\u003c/span\u003e\u003cspan class=\"p\"\u003e++;\u003c/span\u003e            \u003cspan class=\"c1\"\u003e// increment the counter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_value\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e       \u003cspan class=\"c1\"\u003e// return the value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: explains why this shape was chosen\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003elong\u003c/span\u003e \u003cspan class=\"n\"\u003eNext\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Pre-increment is required: downstream subscribers index events by the\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// post-increment value (see docs/adr/0014-event-sequence-numbers.md).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Switching to post-increment silently corrupts replay across the cluster.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eInterlocked\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIncrement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eref\u003c/span\u003e \u003cspan class=\"n\"\u003e_value\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eTest names that are sentences.\u003c/strong\u003e A test name is documentation the build refuses to let you forget. It survives every refactor of the implementation, every renaming, every change of test framework:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eTaxCalculatorTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Fact]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateTax_OnRefundedOrder_ShouldNotProduceNegativeTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e100\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIsRefunded\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esut\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTaxCalculator\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTaxRate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0.19\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003esut\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eShould\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eBeGreaterThanOrEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen someone tries to \u0026ldquo;simplify\u0026rdquo; the calculator years later, this test fails with a name that names the violated constraint.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeliberate naming over generic naming.\u003c/strong\u003e \u003ccode\u003eOrderService\u003c/code\u003e tells the reader nothing. \u003ccode\u003eRetryingOrderSubmitter\u003c/code\u003e tells them where the design effort went:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eRetryingOrderSubmitter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIOrderGateway\u003c/span\u003e \u003cspan class=\"n\"\u003e_gateway\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAsyncPolicy\u003c/span\u003e \u003cspan class=\"n\"\u003e_retryPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eRetryingOrderSubmitter\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eSubmitAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_retryPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ect\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_gateway\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSubmitAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ect\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNames are the cheapest form of documentation, and the one that survives every refactor short of renaming.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003eREADME.md\u003c/code\u003e next to the code that needs it.\u003c/strong\u003e Not at the repository root, where it gets stale within months. Next to the subsystem it describes: a \u003ccode\u003eREADME.md\u003c/code\u003e in the folder of the projection engine, the message dispatcher, the retry layer. Short. Three paragraphs. What this code is for, the one or two constraints that aren\u0026rsquo;t obvious, the decision record that explains the shape. Engineers who never read the top-level README will read the one that lives in the folder they just opened, because it\u0026rsquo;s already on their screen.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-question-ive-started-asking\"\u003e\u003ca href=\"/posts/code-as-legacy-after-you-leave/#the-question-ive-started-asking\" title=\"The Question I\u0026rsquo;ve Started Asking\"\u003eThe Question I\u0026rsquo;ve Started Asking\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBefore I commit anything substantial, I ask:\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eIf I stopped working on this tomorrow, with no warning, no handoff: what would the next person need from this code to keep it working?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s not morbid. It\u0026rsquo;s planning. The probability that I stop working on any given codebase tomorrow is low for any specific tomorrow but approaches one over a long enough horizon.\u003c/p\u003e\n\u003cp\u003eThe question reframes a lot of decisions. The \u003ccode\u003e// TODO\u003c/code\u003e without a ticket: would the next person know what it meant? No. The magic constant: would they know why this number? No. The test I would have skipped: would they know which edge cases were considered? Not without it.\u003c/p\u003e\n\u003cp\u003eThe shape of a method changes once you write for someone who can\u0026rsquo;t ask you anything:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Before: works for me, today, with the context I have\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003erate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0.19\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003erate\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// After: works for a stranger, in 2030, with no one to ask\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// Calculates VAT for a single order using the rate active on the order\u0026#39;s\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;see cref=\u0026#34;Order.PlacedAt\u0026#34;/\u0026gt; date. Historical rates are intentional:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// invoices issued before a rate change must reproduce the original amount\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// (see docs/adr/0021-historical-vat-rates.md).\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;/summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateVatAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003erate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_vatRates\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetRateForAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePlacedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRound\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003erate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eMidpointRounding\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToEven\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe functional difference is small. The legacy difference is enormous. The second version answers every question a stranger could reasonably have: what is calculated, against which rate, with what rounding, justified by which decision record, cancellable when the caller gives up. The first produces the same number on the happy path and a multi-hour archaeology project the first time it doesn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eCareful\u003c/em\u003e, in the end, mostly means: \u003cem\u003elegible to someone who doesn\u0026rsquo;t get to ask you anything\u003c/em\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-larger-legacy\"\u003e\u003ca href=\"/posts/code-as-legacy-after-you-leave/#the-larger-legacy\" title=\"The Larger Legacy\"\u003eThe Larger Legacy\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAcross a career, the code I write will outlast me at every employer I had. Some will outlast the employers themselves; some will be inherited by acquirers, ported to new languages, embedded in libraries someone else maintains. The chain is longer than I imagine while writing.\u003c/p\u003e\n\u003cp\u003eI am not famous. I am not writing a foundational library. I write the unglamorous middle layer of enterprise applications. And even so, the chain is long.\u003c/p\u003e\n\u003cp\u003eThe legacy isn\u0026rsquo;t theoretical. It\u0026rsquo;s just what code does. It survives. It accumulates strangers. It either repays them for the time they spend reading it, or it taxes them.\u003c/p\u003e\n\u003cp\u003eThe motto from part one, with one final addition:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eThe code you create is a valuable legacy.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAnd, after \u003ca href=\"/posts/code-as-legacy-age-of-ai/\"\u003epart four\u003c/a\u003e:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eThe code you accept is a valuable legacy.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe version that finishes the thought:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eThe code you leave behind is a valuable legacy: to people you will never meet, building on work you no longer remember, at companies you no longer work for.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThat\u0026rsquo;s the population I\u0026rsquo;m writing for, in the long run. They don\u0026rsquo;t know me. They never will. The least I can do is write something they can read.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThis is part five of the \u003ca href=\"/posts/code-as-legacy/\"\u003eCode as Legacy\u003c/a\u003e series. \u003ca href=\"/posts/code-as-legacy/\"\u003ePart one\u003c/a\u003e is the manifesto. \u003ca href=\"/posts/code-as-legacy-past-self/\"\u003ePart two\u003c/a\u003e introduces Past Self. \u003ca href=\"/posts/code-as-legacy-empty-promises/\"\u003ePart three\u003c/a\u003e is about empty promises. \u003ca href=\"/posts/code-as-legacy-age-of-ai/\"\u003ePart four\u003c/a\u003e is about the AI in the room.\u003c/em\u003e\u003c/p\u003e\n","date_modified":"2026-06-09T17:00:13+02:00","date_published":"2026-06-09T17:00:00+02:00","id":"https://daily-devops.net/posts/code-as-legacy-after-you-leave/","language":"en","summary":"Every company I've worked for has forgotten most of what I did. The code hasn't. Your name is still in the commit log, and no one can ask you anything.\n","tags":["softwareengineering","codequality","technicaldebt","architecture","dotnet","csharp","bestpractices","ai-code-assistant","github-copilot"],"title":"The Codebase Doesn't Know You Quit\n","url":"https://daily-devops.net/posts/code-as-legacy-after-you-leave/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eI closed \u003ca href=\"/posts/code-as-legacy-empty-promises/\"\u003epart three\u003c/a\u003e with a sentence I now find slightly dishonest:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eI\u0026rsquo;m done adding to the pile deliberately.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIt assumes I\u0026rsquo;m the only one adding to the pile. That stopped being true a few years ago.\u003c/p\u003e\n\u003cp\u003eThe first three parts of the \u003ca href=\"/posts/code-as-legacy/\"\u003e\u003cem\u003eCode as Legacy\u003c/em\u003e\u003c/a\u003e series were about an engineer who left code without context, and the engineer who had to live with it. The argument still holds. What\u0026rsquo;s changed is who else is writing the code that has my name on it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-new-author-in-the-room\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#the-new-author-in-the-room\" title=\"The New Author in the Room\"\u003eThe New Author in the Room\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eA typical commit of mine in 2026: I type three characters of a method name, the editor offers eight lines of plausible C#, I read them, press Tab. My name goes on the commit. The PR gets merged.\u003c/p\u003e\n\u003cp\u003eI wrote zero of those eight lines. I accepted them.\u003c/p\u003e\n\u003cp\u003eA meaningful percentage of any repository I touch was suggested by a model, accepted by a human, and shipped under that human\u0026rsquo;s git identity. The audit trail is coherent and technically incomplete.\u003c/p\u003e\n\u003cp\u003eThis is not a complaint. Coding assistants make me faster, I use them daily. What I want to be honest about is the asymmetry they introduce.\u003c/p\u003e\n\u003cp\u003ePast Self, the engineer from \u003ca href=\"/posts/code-as-legacy-past-self/\"\u003epart two\u003c/a\u003e, was at least a person. He had context, even if he forgot to write it down. You could in principle interrogate him. The new Past Self can\u0026rsquo;t be interrogated, because he isn\u0026rsquo;t anyone.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"code-without-a-why\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#code-without-a-why\" title=\"Code Without a Why\"\u003eCode Without a Why\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe most uncomfortable property of AI-generated code is that the \u003cem\u003ewhy\u003c/em\u003e never existed.\u003c/p\u003e\n\u003cp\u003eWhen a human writes a magic constant, there\u0026rsquo;s usually a story behind it: a measurement, a meeting, a Slack thread. The story is invisible in the code, but it existed and can sometimes be reconstructed.\u003c/p\u003e\n\u003cp\u003eWhen a model writes the same constant, there\u0026rsquo;s no story. The number is the median of similar numbers in similar code. Plausible. Not motivated. Identical in the diff to a number a human chose for a reason:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Could be either: a measured client SLA, or a number a model picked because it\u0026#39;s\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// the median timeout in publicly available .NET code. The diff doesn\u0026#39;t tell you which.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003eReportTimeout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis isn\u0026rsquo;t the model\u0026rsquo;s fault. The fault, if there is one, is in the moment of acceptance: when I take the suggestion without asking whether thirty seconds is right for \u003cem\u003ethis\u003c/em\u003e report, \u003cem\u003ethis\u003c/em\u003e SLA. Acceptance is the authorship event.\u003c/p\u003e\n\u003cp\u003eI have caught myself accepting suggestions that compiled and looked reasonable without being able to articulate why each line was correct. I would not have accepted that diff from a colleague without questions. I accepted it from the editor because the editor doesn\u0026rsquo;t push back.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s a new kind of laziness, and it\u0026rsquo;s mine.\u003c/p\u003e\n\u003cp\u003eThe same pattern surfaces in choices that look stylistic but encode behavior. A \u003ccode\u003eConfigureAwait(false)\u003c/code\u003e on a library call. A \u003ccode\u003ePolly\u003c/code\u003e retry policy with three attempts and exponential backoff. A \u003ccode\u003eJsonSerializerOptions\u003c/code\u003e instance constructed inline instead of cached. Each is defensible. Each is also defended by nothing: no benchmark, no incident report, no policy document. Just the model\u0026rsquo;s pattern-match against code it has seen. The suggestion \u003cem\u003elooks\u003c/em\u003e like the result of a decision because it has the shape of one. That shape is borrowed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-the-suggestion-hides\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#what-the-suggestion-hides\" title=\"What the Suggestion Hides\"\u003eWhat the Suggestion Hides\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe hidden cost in AI-assisted code isn\u0026rsquo;t bugs. The hidden cost is the \u003cem\u003edefaults the model picks when it has no way to know better\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe plausible exception swallow.\u003c/strong\u003e When I pause inside a \u003ccode\u003etry\u003c/code\u003e block:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Failed to process\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCompiles. Looks responsible. Returns \u003ccode\u003enull\u003c/code\u003e to a caller that wasn\u0026rsquo;t expecting it, three call frames up. The model offered the shape of error handling, but it can\u0026rsquo;t know whether \u003ccode\u003enull\u003c/code\u003e is right here, because \u003cem\u003eright\u003c/em\u003e depends on the contract of the calling code, which it can\u0026rsquo;t see.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe test that asserts what it sees.\u003c/strong\u003e Ask a model to write tests for an existing method. It reads the method, infers the behavior, writes tests that pass. If the method is wrong, the tests are wrong in the same direction:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Method (subtly wrong: rounds half-down instead of half-up)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateTax\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRound\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eTaxRate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Test produced by reading the method, not the spec\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Fact]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateTax_OnFiftyCents_ReturnsTwentyFour\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_service\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateTax\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0.5\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eShould\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eBe\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0.24\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// passes; spec says 0.25m\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTest green. Bug ships. Reviewer assumes coverage.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe hallucinated API.\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Suggested: looks like a perfectly reasonable BCL method\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eupdated\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003edictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryUpdateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enewValue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eoldValue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Reality: TryUpdate exists only on ConcurrentDictionary\u0026lt;TKey, TValue\u0026gt;,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// it\u0026#39;s synchronous, and there is no async overload.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edictionary\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003eConcurrentDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econcurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003econcurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryUpdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enewValue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eoldValue\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe compiler stops these. A low-grade tax on attention.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe deprecated pattern in confident clothing.\u003c/strong\u003e The model was trained on a lot of \u003ccode\u003eWebClient\u003c/code\u003e, a lot of \u003ccode\u003eNewtonsoft.Json\u003c/code\u003e, a lot of \u003ccode\u003eIHttpClientFactory\u003c/code\u003e-less \u003ccode\u003enew HttpClient()\u003c/code\u003e calls. It will suggest them fluently in 2026:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// What the editor offers (compiles, ships, leaks sockets under load)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eclient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHttpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejson\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStringAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeserializeObject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejson\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// What the team actually agreed on three years ago\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eReportClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIHttpClientFactory\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClientFactory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eclient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClientFactory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eReportClient\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eclient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetFromJsonAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBoth are technically valid. The first gets flagged by anyone paying attention. Accepted silently, it\u0026rsquo;s legacy on the day it ships.\u003c/p\u003e\n\u003cp\u003eThe model is doing what it was built to do: produce plausible code. \u003cem\u003ePlausible\u003c/em\u003e is not a synonym for \u003cem\u003ecorrect in this context\u003c/em\u003e. That distinction is the entire job.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-accountability-hole\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#the-accountability-hole\" title=\"The Accountability Hole\"\u003eThe Accountability Hole\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003egit blame\u003c/code\u003e still returns a human: the one who accepted the suggestion. That human, often, will look at the line and say honestly, \u003cem\u003eI don\u0026rsquo;t remember writing this\u003c/em\u003e. And they won\u0026rsquo;t, because they didn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eThe hole isn\u0026rsquo;t legal. The committer is legally responsible. The hole is \u003cem\u003eepistemic\u003c/em\u003e: no one to interview, no Slack thread to find, no meeting where the decision was made. The decision was never made; the code just appeared, plausibly enough that no one stopped it.\u003c/p\u003e\n\u003cp\u003eI have run this experiment on myself, in a small way. I went back to a service I\u0026rsquo;d worked on six months earlier, picked five lines at random, and tried to explain why each one was the way it was. Three I could justify on the spot. One I had to read the surrounding code for. One I had no answer to. I had accepted a suggestion, the line was reasonable, and I could not recover any reasoning behind the specific shape it took. The line had been in production for half a year.\u003c/p\u003e\n\u003cp\u003eIf part two\u0026rsquo;s argument was that Past Self left a thin trail of context, the new problem is that the trail might be empty.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-hasnt-changed\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#what-hasnt-changed\" title=\"What Hasn\u0026rsquo;t Changed\"\u003eWhat Hasn\u0026rsquo;t Changed\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe author has multiplied. The responsibility has not.\u003c/p\u003e\n\u003cp\u003eWhen I accept a Copilot suggestion, I am the author for every purpose that matters. The git log says so. The maintenance burden says so. The engineer who curses the line at 3 AM will be cursing me, correctly. The model will not be paged. It will not sit in the post-mortem.\u003c/p\u003e\n\u003cp\u003eI will.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThe code you create is a valuable legacy\u003c/em\u003e still holds. \u0026ldquo;Create\u0026rdquo; includes \u0026ldquo;accept on someone else\u0026rsquo;s behalf.\u0026rdquo; Pressing Tab is an act of authorship. Treating it as anything else is the new lie: \u003cem\u003eI\u0026rsquo;ll come back to this later\u003c/em\u003e becomes \u003cem\u003ethe model wrote it, not me\u003c/em\u003e. Both serve the same function: letting me ship code I haven\u0026rsquo;t fully thought about without feeling like the one who shipped it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-im-trying-to-do-differently\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#what-im-trying-to-do-differently\" title=\"What I\u0026rsquo;m Trying to Do Differently\"\u003eWhat I\u0026rsquo;m Trying to Do Differently\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe practices from earlier in this series still apply. Roslyn analyzers still catch what they always caught. The \u003ccode\u003e// TODO\u003c/code\u003e discipline still holds (and matters more, because models love to leave them). What I\u0026rsquo;ve added is a small set of habits aimed at the AI gap.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRead every suggestion before accepting, slowly enough to articulate the \u003cem\u003ewhy\u003c/em\u003e.\u003c/strong\u003e If I can\u0026rsquo;t say in one sentence why this line is right, I shouldn\u0026rsquo;t accept it. The friction is the point.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTreat AI output as a PR from a contractor with no domain context.\u003c/strong\u003e Competent generalist, never seen the codebase, doesn\u0026rsquo;t know the SLA. I review accordingly.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNever let AI write the tests for the code it just wrote.\u003c/strong\u003e Both halves from the same source produce a closed loop where the tests can\u0026rsquo;t catch what\u0026rsquo;s wrong. In practice this means one of two protocols: I write the implementation and let the model help with tests, or the model drafts the implementation and I write the tests against the specification, never both from the same prompt.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAsk the model to explain its reasoning before committing.\u003c/strong\u003e Sanity check, not ceremony. If the explanation reveals an assumption I missed (a library version, a threading model, a \u003ccode\u003eSystem.Text.Json\u003c/code\u003e serialization quirk), that\u0026rsquo;s the moment to course-correct. If the explanation is hand-waving (\u003cem\u003ethis is a common pattern\u003c/em\u003e, \u003cem\u003ethis is generally how it\u0026rsquo;s done\u003c/em\u003e), the code probably is too.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReserve certain paths as no-autocomplete zones.\u003c/strong\u003e Authentication, authorization, anything that touches money or crosses a trust boundary:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Authorize(Policy = \u0026#34;InvoiceWrite\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eAdjustInvoice\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e \u003cspan class=\"n\"\u003einvoiceId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [FromBody]\u003c/span\u003e \u003cspan class=\"n\"\u003eInvoiceAdjustment\u003c/span\u003e \u003cspan class=\"n\"\u003eadjustment\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003einvoice\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_invoices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einvoiceId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einvoice\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorization\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorizeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einvoice\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;CanAdjust\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eForbid\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003einvoice\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eApply\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eadjustment\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_clock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_invoices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einvoice\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNoContent\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEvery line is a policy decision: which attribute, which policy name, which authorization handler, which order, which result type leaks information. The cost of being plausibly wrong here is unbounded.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t rules I always follow. They\u0026rsquo;re rules I\u0026rsquo;m trying to follow.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-legacy-is-still-mine\"\u003e\u003ca href=\"/posts/code-as-legacy-age-of-ai/#the-legacy-is-still-mine\" title=\"The Legacy Is Still Mine\"\u003eThe Legacy Is Still Mine\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThere\u0026rsquo;s a tempting fantasy that the model will eventually be good enough that this doesn\u0026rsquo;t matter. I don\u0026rsquo;t buy it. The human still makes the acceptance decision, and acceptance is the authorship event. Better suggestions make better-looking code that\u0026rsquo;s easier to accept without thinking. The temptation grows with the quality.\u003c/p\u003e\n\u003cp\u003eThe work doesn\u0026rsquo;t go away. It just gets harder to remember to do.\u003c/p\u003e\n\u003cp\u003eI will inherit code in 2030 that was suggested by a 2026 model and accepted by a 2026 version of me who was tired, under pressure, or too willing to trust the green checkmark. That code will have my name on it. I won\u0026rsquo;t remember writing it. It will still be my legacy.\u003c/p\u003e\n\u003cp\u003eThe 2030 version of me will not be able to tell which lines I composed and which I waved through. The \u003ccode\u003egit log\u003c/code\u003e won\u0026rsquo;t help: every line reads \u003ccode\u003eAuthor: Martin Stühmer\u003c/code\u003e. The reasoning trail won\u0026rsquo;t help either, because there was never one for half of them. What he will have is the same artifact every stranger has: the code, on a screen, at 3 AM, with a customer on the phone. He will have to defend it on its own terms, regardless of who or what originally produced it.\u003c/p\u003e\n\u003cp\u003eThe motto from part one, with one revision:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eThe code you accept is a valuable legacy, so it\u0026rsquo;s important to accept it carefully.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThat\u0026rsquo;s the part of the job the machine can\u0026rsquo;t take from me. It\u0026rsquo;s also the part I\u0026rsquo;d most like to delegate. The discipline is in not doing so.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThis is part four of the \u003ca href=\"/posts/code-as-legacy/\"\u003eCode as Legacy\u003c/a\u003e series. \u003ca href=\"/posts/code-as-legacy/\"\u003ePart one\u003c/a\u003e covers what \u0026ldquo;building carefully\u0026rdquo; actually means. \u003ca href=\"/posts/code-as-legacy-past-self/\"\u003ePart two\u003c/a\u003e introduces Past Self. \u003ca href=\"/posts/code-as-legacy-empty-promises/\"\u003ePart three\u003c/a\u003e is about empty promises.\u003c/em\u003e\u003c/p\u003e\n","date_modified":"2026-06-04T17:05:04+02:00","date_published":"2026-06-04T17:00:00+02:00","id":"https://daily-devops.net/posts/code-as-legacy-age-of-ai/","language":"en","summary":"Copilot and Claude finish methods before I do, shipping code under my name. What changes when the author is partly a machine, and what doesn't.\n","tags":["softwareengineering","codequality","technicaldebt","architecture","dotnet","csharp","bestpractices","ai-code-assistant","github-copilot"],"title":"The Machine Writes. The Legacy Is Still Mine.\n","url":"https://daily-devops.net/posts/code-as-legacy-age-of-ai/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLet me describe a workflow I\u0026rsquo;m sure you recognize.\u003c/p\u003e\n\u003cp\u003eYou ask Claude or Copilot to implement something: a service method, a repository, a handler. The generated code looks right. Compiles cleanly. You review it, it seems reasonable, you ask the agent to write tests for it too. The tests come back neat and organized. You run them. Green. You move on.\u003c/p\u003e\n\u003cp\u003eThree weeks later, a production incident. The implementation had an edge case nobody thought to test. The agent didn\u0026rsquo;t know about it because it wasn\u0026rsquo;t in the prompt. The review missed it because the code looked correct. The tests didn\u0026rsquo;t catch it because they verified what the implementation does, not what it was supposed to do.\u003c/p\u003e\n\u003cp\u003eThat gap (between \u0026ldquo;code that works in the demo\u0026rdquo; and \u0026ldquo;code that holds up in production\u0026rdquo;) is supposed to be what testing closes. And in an AI-assisted workflow, that\u0026rsquo;s exactly the gap that gets skipped.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-part-im-embarrassed-to-admit\"\u003e\u003ca href=\"/posts/tunit-ai-coding-agents/#the-part-im-embarrassed-to-admit\" title=\"The Part I\u0026rsquo;m Embarrassed to Admit\"\u003eThe Part I\u0026rsquo;m Embarrassed to Admit\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhen I\u0026rsquo;m deep in an AI-assisted development flow, testing is where my discipline slips first.\u003c/p\u003e\n\u003cp\u003eThe implementation is complex. Maybe three services interact, there\u0026rsquo;s a cache in the middle, and the whole thing is async. Writing a thorough test suite means mentally stepping through every combination: what happens when the cache is cold, when the downstream service is slow, when two requests arrive simultaneously. That\u0026rsquo;s real work. It takes time.\u003c/p\u003e\n\u003cp\u003eSo I test the happy path. The one where everything cooperates. And I tell myself I\u0026rsquo;ll add the edge cases later.\u003c/p\u003e\n\u003cp\u003eLater never comes. The next feature is already in progress, the backlog has moved on, and \u0026ldquo;tests are green\u0026rdquo; becomes the end of the story, even when the tests were never really trying.\u003c/p\u003e\n\u003cp\u003eThis is the actual failure mode of AI-assisted velocity. Not that the agent writes bad code, but that it helps you ship faster than your test discipline can keep up. The agent generates, you review, the tests pass, you deploy. Somewhere in that chain, the hard questions stop being asked.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-tests-pass-means-less-than-it-used-to\"\u003e\u003ca href=\"/posts/tunit-ai-coding-agents/#why-tests-pass-means-less-than-it-used-to\" title=\"Why \u0026ldquo;Tests Pass\u0026rdquo; Means Less Than It Used To\"\u003eWhy \u0026ldquo;Tests Pass\u0026rdquo; Means Less Than It Used To\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe traditional assumption behind a passing test suite was that the person writing the tests understood what they were testing. They had context. They knew the edge cases from experience with the system. Their tests were incomplete, sure, but they were at least trying to model reality.\u003c/p\u003e\n\u003cp\u003eWhen an AI agent writes both the implementation and the tests, that assumption breaks. The agent generates tests that are consistent with the implementation: internally coherent, but not necessarily correct. The implementation handles the happy path cleanly, so the tests verify the happy path. Everything agrees. Nothing is wrong, technically. And yet the thing you actually needed tested isn\u0026rsquo;t covered.\u003c/p\u003e\n\u003cp\u003eGreen CI stops meaning \u0026ldquo;this works\u0026rdquo; and starts meaning \u0026ldquo;this is internally consistent.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s a subtle but important shift. And most test frameworks don\u0026rsquo;t help you notice it.\u003c/p\u003e\n\u003cp\u003eThe worst part is what that invisible failure looks like from the outside. CI is green. Coverage looks reasonable. The PR merged cleanly. Production breaks a week later on an edge case nobody thought to test. The infrastructure worked exactly as designed. It was testing the wrong thing, faithfully.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-framework-problem\"\u003e\u003ca href=\"/posts/tunit-ai-coding-agents/#the-framework-problem\" title=\"The Framework Problem\"\u003eThe Framework Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMSTest, xUnit, and NUnit were designed for a slower workflow: deliberate, human-written code, maintained by people who ran the tests constantly and understood what they were testing. Their architecture reflects that.\u003c/p\u003e\n\u003cp\u003eDiscovery happens at runtime via reflection. That sounds fine until you realize what it means in a high-velocity AI-assisted workflow: a test that got silently refactored away, or had its \u003ccode\u003e[Test]\u003c/code\u003e attribute removed by a code-generating agent, simply vanishes from your test suite. The binary compiles. CI runs. Zero tests fail. Zero tests run for that module. Nobody notices until production breaks and someone asks \u0026ldquo;didn\u0026rsquo;t we have tests for this?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eTest ordering is implicit. An AI-generated integration test might assume a database record already exists, or an event already fired. If that assumption isn\u0026rsquo;t expressed anywhere, the test either becomes flaky (passing when run in a certain order, failing otherwise) or it accidentally passes by relying on leftover state from another test. Both are worse than a clean failure.\u003c/p\u003e\n\u003cp\u003eAsync is bolted on rather than native. Modern .NET code is overwhelmingly async, and AI agents generate async code naturally. When the framework makes async awkward, you get \u003ccode\u003eGetAwaiter().GetResult()\u003c/code\u003e workarounds, sync-over-async anti-patterns, or tests that appear to pass without actually awaiting the thing they\u0026rsquo;re testing. Every one of those is a quiet bug.\u003c/p\u003e\n\u003cp\u003eNone of these are exotic edge cases. They\u0026rsquo;re the everyday failure modes of working fast.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-tunit-fits\"\u003e\u003ca href=\"/posts/tunit-ai-coding-agents/#where-tunit-fits\" title=\"Where TUnit Fits\"\u003eWhere TUnit Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://tunit.dev/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eTUnit\u003c/a\u003e is a .NET testing framework built on Roslyn source generators and \u003ccode\u003eMicrosoft.Testing.Platform\u003c/code\u003e. I reach for it in AI-assisted workflows not because of benchmarks, but because its design choices address the actual failure modes I keep running into.\u003c/p\u003e\n\u003cp\u003eThe first is \u003cstrong\u003ecompile-time test discovery\u003c/strong\u003e. TUnit generates the test catalog at build time, not runtime. If a test disappears because an agent refactored it away (\u003ccode\u003e[Test]\u003c/code\u003e attribute gone, method renamed, class restructured), you find out at \u003ccode\u003edotnet build\u003c/code\u003e. Not after a green CI run that silently covered nothing. That single shift in when problems surface makes a real practical difference when you\u0026rsquo;re merging fast.\u003c/p\u003e\n\u003cp\u003eThe second is \u003ccode\u003e[DependsOn]\u003c/code\u003e, which lets you express ordering assumptions explicitly rather than leaving them implicit in fixture setup:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderServiceTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateOrder_ShouldPersistToDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_service\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eProductId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eQuantity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNotNull\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DependsOn(nameof(CreateOrder_ShouldPersistToDatabase))]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eFulfillOrder_ShouldUpdateStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_service\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetFirstPendingAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_service\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFulfillAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFulfilled\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf \u003ccode\u003eCreateOrder_ShouldPersistToDatabase\u003c/code\u003e fails, the dependent test is skipped automatically. The signal is clean: the precondition failed. Not a cascade of ten tests all failing for the same root cause, pointing in ten different directions.\u003c/p\u003e\n\u003cp\u003eThe third is \u003cstrong\u003enative async throughout\u003c/strong\u003e. Setup, teardown, test methods: all async without ceremony. No workarounds, no wrappers, no subtle deadlocks hiding in \u003ccode\u003eGetAwaiter().GetResult()\u003c/code\u003e. The framework works with the shape of modern .NET code instead of against it.\u003c/p\u003e\n\u003cp\u003eThe fourth, and the one that surprises people most: \u003cstrong\u003eparallelism by default\u003c/strong\u003e. Most teams see this as a performance optimization. In practice it\u0026rsquo;s a bug detector. AI-generated code regularly introduces shared mutable state: static caches, singleton misuse, dictionaries that weren\u0026rsquo;t designed for concurrent access. Code that passes every serial test fails immediately under parallelism. Which is also exactly how it fails in production under real load.\u003c/p\u003e\n\u003cp\u003eRunning parallel tests on day one of a migration is uncomfortable. It\u0026rsquo;s also the most diagnostic step you can take.\u003c/p\u003e\n\u003cp\u003eWhen you migrate a codebase to TUnit and tests that \u0026ldquo;always worked\u0026rdquo; suddenly fail, that\u0026rsquo;s not TUnit being difficult. That\u0026rsquo;s TUnit showing you something that was always broken and never visible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-tunit-still-cant-do\"\u003e\u003ca href=\"/posts/tunit-ai-coding-agents/#what-tunit-still-cant-do\" title=\"What TUnit Still Can\u0026rsquo;t Do\"\u003eWhat TUnit Still Can\u0026rsquo;t Do\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNone of this fixes the original problem. If I ask an agent to generate tests for an implementation it also generated, I\u0026rsquo;ll get a test suite that\u0026rsquo;s consistent with the code and tests the happy path. TUnit will run those tests faithfully and report green. Compile-time discovery doesn\u0026rsquo;t make bad tests good. It just prevents tests from silently disappearing.\u003c/p\u003e\n\u003cp\u003eThe edge cases still have to come from somewhere. The scenario where the cache is cold and the fallback throws. The concurrent update that corrupts state. The record that arrived in an unexpected status. Those tests come from knowing what actually breaks: from experience, from incident post-mortems, from the kind of system knowledge that doesn\u0026rsquo;t live in a prompt.\u003c/p\u003e\n\u003cp\u003eMutation testing is one way to pressure-test what you have. \u003ca href=\"/posts/tests-are-lying/\"\u003e\u0026ldquo;Your Tests Are Lying\u0026rdquo;\u003c/a\u003e covers the approach in detail. The short version: if removing a line of business logic doesn\u0026rsquo;t make any test fail, that logic was never really being tested, regardless of how many tests you have or which framework runs them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"so-should-you-switch\"\u003e\u003ca href=\"/posts/tunit-ai-coding-agents/#so-should-you-switch\" title=\"So, Should You Switch?\"\u003eSo, Should You Switch?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re on a stable xUnit or MSTest codebase with a thorough test suite, TUnit is not an emergency. The \u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/\"\u003epragmatic evaluation\u003c/a\u003e covers the migration tradeoffs and timing in detail.\u003c/p\u003e\n\u003cp\u003eThe case for switching is strongest when: you\u0026rsquo;re actively using AI coding agents and your code volume has gone up significantly; your test suite is growing fast but you\u0026rsquo;re not confident it\u0026rsquo;s growing in the right directions; you\u0026rsquo;ve had more than one \u0026ldquo;the tests passed but production broke\u0026rdquo; conversation recently; or you\u0026rsquo;re fighting async friction, flaky ordering, or slow discovery in large suites.\u003c/p\u003e\n\u003cp\u003eIn those cases, TUnit\u0026rsquo;s architecture addresses the actual shape of the problem rather than just running the tests faster.\u003c/p\u003e\n\u003cp\u003eThe productivity gains from AI coding agents are real. So is the cost. You\u0026rsquo;re shipping more code. Some of it is wrong. The wrong parts get through review because they look right, and the tests pass because they were generated by the same agent that generated the implementation, so of course they agree.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s what AI drift looks like. Not dramatic, not obvious. Just a slow accumulation of code that\u0026rsquo;s never quite been tested for the things that actually break.\u003c/p\u003e\n\u003cp\u003eThe hard questions don\u0026rsquo;t disappear just because an agent answered the easy ones. What happens when the cache is cold and the fallback throws? What if two requests modify the same record simultaneously? What if the record arrived in a state the happy-path test never set up? Those questions don\u0026rsquo;t appear in a prompt. They appear in production, at the worst possible time.\u003c/p\u003e\n\u003cp\u003eTUnit doesn\u0026rsquo;t solve that. But it makes the infrastructure honest: tests that can\u0026rsquo;t disappear quietly, assumptions that have to be declared, concurrency that surfaces on your laptop instead of in production. That\u0026rsquo;s the environment you want when you\u0026rsquo;re moving fast and you know your discipline is the thing under pressure.\u003c/p\u003e\n\u003cp\u003eThe happy path tests aren\u0026rsquo;t enough. You already know that, which is probably why you\u0026rsquo;re reading this.\u003c/p\u003e\n","date_modified":"2026-06-02T20:27:33+02:00","date_published":"2026-06-02T17:00:00+02:00","id":"https://daily-devops.net/posts/tunit-ai-coding-agents/","language":"en","summary":"AI coding agents make you ship faster. They make your bugs faster too. Generated tests verify what the code does, not what it should. Here's why TUnit helps.\n","tags":["dotnet","testing","ai","ai-code-assistant","softwareengineering","bestpractices","csharp"],"title":"You're Shipping Bugs Faster, and Your Tests Are Helping\n","url":"https://daily-devops.net/posts/tunit-ai-coding-agents/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eTwo articles into this series, I\u0026rsquo;ve spent a lot of words describing Past Self, the engineer who left the evidence file, who optimized for the wrong horizon, who handed Future Self the rough work without the context to do it properly.\u003c/p\u003e\n\u003cp\u003eWhat I\u0026rsquo;ve been carefully avoiding is the obvious conclusion.\u003c/p\u003e\n\u003cp\u003eI am Past Self. Right now. Today. The \u003ccode\u003e// TODO\u003c/code\u003e I wrote last Tuesday is already starting to decay. The verbal commitment I made in last week\u0026rsquo;s planning session: \u0026ldquo;we\u0026rsquo;ll revisit that architecture after the next release\u0026rdquo;. It has already begun its quiet journey toward never. The test coverage gap I noted and deprioritized is waiting to become an incident.\u003c/p\u003e\n\u003cp\u003eI know this because I\u0026rsquo;ve read the code Past Self wrote, and I recognize the voice.\u003c/p\u003e\n\u003cp\u003eIt sounds exactly like mine.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-promises-ive-made\"\u003e\u003ca href=\"/posts/code-as-legacy-empty-promises/#the-promises-ive-made\" title=\"The Promises I\u0026rsquo;ve Made\"\u003eThe Promises I\u0026rsquo;ve Made\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;m not going to pretend these are abstract patterns. They\u0026rsquo;re mine.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e// TODO: implement proper tiered discount logic\u003c/code\u003e. I wrote that. Three years ago. The \u0026ldquo;proper\u0026rdquo; logic was never defined, never ticketed, never implemented. The method has run in production millions of times with the simplified version. I told myself it was a placeholder. It became the implementation.\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;We\u0026rsquo;ll add observability to this once the service stabilizes\u0026rdquo;: I said that in a meeting in 2023. The service stabilized. The observability never materialized. Six months later we had an incident where the first question was \u0026ldquo;what is this service actually doing right now\u0026rdquo; and the answer was silence. I remembered the promise the moment someone asked the question. I did not say anything in the post-mortem about having made it.\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;I\u0026rsquo;ll write the integration tests for this edge case next sprint\u0026rdquo;. The edge case was in a payment calculation, the kind of thing where being wrong has a number attached to it. Next sprint arrived with different priorities. The sprint after that as well. The test was never written. The bug in the edge case was found by a customer, not by us.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t cautionary tales about other engineers. They\u0026rsquo;re mine. The damage was real, the promises were mine, and the fact that I meant them at the time doesn\u0026rsquo;t change what Future Self found when he arrived.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-meaning-it-is-worth\"\u003e\u003ca href=\"/posts/code-as-legacy-empty-promises/#what-meaning-it-is-worth\" title=\"What \u0026ldquo;Meaning It\u0026rdquo; Is Worth\"\u003eWhat \u0026ldquo;Meaning It\u0026rdquo; Is Worth\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is the part that took me the longest to accept: intent is not load-bearing.\u003c/p\u003e\n\u003cp\u003eWhen I wrote \u003ccode\u003e// TODO: fix this properly\u003c/code\u003e, I genuinely intended to come back to it. When I said \u0026ldquo;we\u0026rsquo;ll refactor after the release,\u0026rdquo; I believed, in that moment, that we would. I wasn\u0026rsquo;t lying. I was optimistic, or under pressure, or operating with a timeline I thought was realistic.\u003c/p\u003e\n\u003cp\u003eBut Future Self doesn\u0026rsquo;t inherit my intentions. He inherits the code.\u003c/p\u003e\n\u003cp\u003eHe doesn\u0026rsquo;t know that I meant it. He doesn\u0026rsquo;t know that the promise was sincere. He finds a \u003ccode\u003e// TODO\u003c/code\u003e comment with no ticket, no context, no owner, and no indication of how dangerous the thing it describes actually is. He finds a service with no observability and has to make decisions in the dark:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO: proper logging\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat \u003ccode\u003ereturn null\u003c/code\u003e is now someone else\u0026rsquo;s NullReferenceException three call frames up, with no stack trace connecting it back here, and no log entry that tells Future Self what the original exception was. He finds a payment calculation with an untested edge case and either notices the gap (in which case he has to stop what he\u0026rsquo;s doing and fix it) or doesn\u0026rsquo;t notice, in which case the customer finds it.\u003c/p\u003e\n\u003cp\u003eMy intentions are invisible to Future Self. What I left behind is not.\u003c/p\u003e\n\u003cp\u003eThat asymmetry is the thing I couldn\u0026rsquo;t keep ignoring.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-decision\"\u003e\u003ca href=\"/posts/code-as-legacy-empty-promises/#the-decision\" title=\"The Decision\"\u003eThe Decision\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;m done with empty promises.\u003c/p\u003e\n\u003cp\u003eNot in the sense of \u0026ldquo;I will now be perfect and never defer anything again\u0026rdquo;. That\u0026rsquo;s just a different kind of empty promise. I mean something more specific: I\u0026rsquo;m done using \u003ccode\u003e// TODO\u003c/code\u003e as a substitute for a decision, and I\u0026rsquo;m done making verbal commitments about future work that has no owner, no trigger, and no cost attached to not delivering.\u003c/p\u003e\n\u003cp\u003eThe shift is smaller than it sounds, and it took me longer than I\u0026rsquo;d like to admit to make it.\u003c/p\u003e\n\u003cp\u003eA \u003ccode\u003e// TODO\u003c/code\u003e without a tracked issue is not a note: it\u0026rsquo;s a lie I\u0026rsquo;m telling Future Self about my intentions. If I can\u0026rsquo;t take sixty seconds to open a ticket, I don\u0026rsquo;t actually believe this is worth doing. So either I create the ticket and reference it, or I delete the comment and accept that this is the implementation. Not both.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Before: a promise to no one, tracked nowhere\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// TODO: implement proper tiered discount logic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e0.1\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// After: a decision, documented\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Simplified discount (full tiered logic tracked in #847)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e0.1\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe code is identical. The difference is honesty. Issue #847 exists, has context, can be prioritized or closed as \u0026ldquo;won\u0026rsquo;t fix.\u0026rdquo; The \u003ccode\u003e// TODO\u003c/code\u003e was a gesture. The issue reference is a commitment that can be held.\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;We\u0026rsquo;ll refactor after the release\u0026rdquo; needs a condition that actually fires, not a timeline that slides. \u0026ldquo;We revisit this when we add the second tenant\u0026rdquo; fires when it fires or it doesn\u0026rsquo;t. If the second tenant never comes, the decision was right. \u0026ldquo;Next sprint\u0026rdquo; never arrives. Conditions arrive or they don\u0026rsquo;t. That\u0026rsquo;s the difference between a trigger and a wish.\u003c/p\u003e\n\u003cp\u003eAnd missing tests aren\u0026rsquo;t a detail I\u0026rsquo;ll get to later. If the test is worth writing, the feature isn\u0026rsquo;t done. That\u0026rsquo;s a discipline question, not a time question. Pretending it\u0026rsquo;s a time question is how the payment edge case goes untested for two years. What I do instead is leave the skeleton visible:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Fact(Skip = \u0026#34;Edge case: negative discount on refunded orders, see #912\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eApplyDiscount_OnRefundedOrder_ShouldNotProduceNegativeTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eNotImplementedException\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis test doesn\u0026rsquo;t pass. It doesn\u0026rsquo;t even run. But it exists, it has a ticket reference, and it fails loudly if someone removes the \u003ccode\u003eSkip\u003c/code\u003e before the implementation is done. The gap is visible, not implicit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-actually-costs\"\u003e\u003ca href=\"/posts/code-as-legacy-empty-promises/#what-this-actually-costs\" title=\"What This Actually Costs\"\u003eWhat This Actually Costs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI want to be honest about something: this decision is not free.\u003c/p\u003e\n\u003cp\u003eMaking it real has friction. Creating a ticket instead of writing a comment takes longer in the moment, not much, but enough to feel it when you\u0026rsquo;re under pressure and someone is waiting for you to ship. Saying \u0026ldquo;I\u0026rsquo;m not going to commit to that refactor without a trigger condition\u0026rdquo; in a planning meeting is harder than saying \u0026ldquo;we\u0026rsquo;ll handle that in Q3.\u0026rdquo; Treating missing tests as a blocker on the definition of done means occasionally shipping later than a version that cuts corners.\u003c/p\u003e\n\u003cp\u003eThe friction is real. What I\u0026rsquo;ve had to accept is that the friction now is cheaper than the silence later.\u003c/p\u003e\n\u003cp\u003eBecause the alternative isn\u0026rsquo;t \u0026ldquo;no friction.\u0026rdquo; The alternative is the post-mortem where nobody mentions the promise that wasn\u0026rsquo;t kept. It\u0026rsquo;s the \u003ccode\u003e// TODO\u003c/code\u003e comment that becomes a fossil, referenced by code that depends on the thing it was promising to fix, until Future Self doesn\u0026rsquo;t know if he can touch it without breaking something he can\u0026rsquo;t see. It\u0026rsquo;s the incident that happens because the edge case was on someone\u0026rsquo;s list.\u003c/p\u003e\n\u003cp\u003eThat friction compounds. The friction of honesty now is roughly constant. The friction of deferred promises grows every month they age.\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s also something harder to quantify: what it does to the people around you. A team that\u0026rsquo;s learned to discount verbal commitments, because they\u0026rsquo;ve seen enough \u0026ldquo;we\u0026rsquo;ll fix that after the release\u0026rdquo; promises expire, stops trusting the ones you mean. You lose the ability to say \u0026ldquo;this will get done\u0026rdquo; and have it land. Past Self made enough empty promises that Future Self, and the people who work with me, have to spend some effort evaluating which commitments are real.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s a cost I inflicted by being careless with my word. Rebuilding it takes longer than the individual tickets I didn\u0026rsquo;t create.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-future-self-deserves\"\u003e\u003ca href=\"/posts/code-as-legacy-empty-promises/#what-future-self-deserves\" title=\"What Future Self Deserves\"\u003eWhat Future Self Deserves\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIn \u003ca href=\"/posts/code-as-legacy-past-self/\"\u003epart two\u003c/a\u003e of this series, I described Future Self as the person who inherits whatever I ship. I know who he is. I know what his days look like. I know what it feels like to find a codebase full of \u003ccode\u003e// TODO\u003c/code\u003e comments with no context, verbal promises that evaporated, coverage gaps that became incidents.\u003c/p\u003e\n\u003cp\u003eI know because I am him, regularly, looking at code Past Self wrote.\u003c/p\u003e\n\u003cp\u003eHe\u0026rsquo;ll show up at 11 PM because something is broken in production, and the first thing he\u0026rsquo;ll hit is a method that has been quietly wrong for two years because the test that would have caught it was on someone\u0026rsquo;s list. He\u0026rsquo;ll have thirty minutes to understand a decision Past Self made under pressure, with no comment, no ticket, no trail. Just a magic constant and a hunch that something here used to make sense:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e30000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThirty seconds. Why thirty? Was it measured? Is it a client SLA? Is it a guess? Is it still right? Future Self has no way to know. He can change it and hope, or leave it and wonder. Past Self knew the answer once. He just didn\u0026rsquo;t write it down. He\u0026rsquo;ll look at the \u003ccode\u003e// TODO\u003c/code\u003e in the error-handling path and wonder, correctly, whether this is load-bearing neglect or just noise.\u003c/p\u003e\n\u003cp\u003eHe deserves better than my good intentions. Not because he\u0026rsquo;s fragile. He isn\u0026rsquo;t. But because every hour he spends excavating my reasoning is an hour he isn\u0026rsquo;t spending building something. Every incident that traces back to a promise I didn\u0026rsquo;t keep is a cost he didn\u0026rsquo;t ask to carry.\u003c/p\u003e\n\u003cp\u003eHe deserves decisions that were documented well enough to be evaluated against the current situation and changed if needed. He deserves to know, when he finds a \u003ccode\u003e// TODO\u003c/code\u003e, whether that represents genuine deferred work tracked somewhere or just a comment Past Self left as a gesture of good faith that nobody else can redeem. He deserves code that doesn\u0026rsquo;t require trust in a person who\u0026rsquo;s no longer present.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not heroism. It\u0026rsquo;s just honesty about what a promise is.\u003c/p\u003e\n\u003cp\u003eA promise you make without infrastructure to keep it isn\u0026rsquo;t a promise. It\u0026rsquo;s a note to yourself that you\u0026rsquo;re leaving someone else\u0026rsquo;s problem for later. I\u0026rsquo;ve left enough of those. Future Self has been cleaning them up for years, and he\u0026rsquo;ll inherit a few more before I get this right.\u003c/p\u003e\n\u003cp\u003eBut I\u0026rsquo;m done adding to the pile deliberately. The accidental ones are unavoidable. You can\u0026rsquo;t know what you don\u0026rsquo;t know yet. The deliberate ones, the \u003ccode\u003e// TODO\u003c/code\u003e you write because it\u0026rsquo;s faster, the commitment you make because it\u0026rsquo;s easier than having the harder conversation right now: those are the ones I\u0026rsquo;m done with.\u003c/p\u003e\n\u003cp\u003eFuture Self is going to inherit my code either way. The question is what kind of Past Self I\u0026rsquo;m choosing to be for him.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThis is part three of the \u003ca href=\"/posts/code-as-legacy/\"\u003eCode as Legacy\u003c/a\u003e series. \u003ca href=\"/posts/code-as-legacy/\"\u003ePart one\u003c/a\u003e covers what \u0026ldquo;building carefully\u0026rdquo; actually means in practice. \u003ca href=\"/posts/code-as-legacy-past-self/\"\u003ePart two\u003c/a\u003e is about Past Self, the person who made the mess.\u003c/em\u003e\u003c/p\u003e\n","date_modified":"2026-05-28T17:06:53+02:00","date_published":"2026-05-28T17:00:00+02:00","id":"https://daily-devops.net/posts/code-as-legacy-empty-promises/","language":"en","summary":"// TODO: fix this properly. We'll refactor after the release. Tests when the API stabilizes. I've made every one of these promises. I'm done.\n","tags":["softwareengineering","codequality","technicaldebt","architecture","dotnet","csharp","bestpractices"],"title":"I'm Done Making Empty Promises\n","url":"https://daily-devops.net/posts/code-as-legacy-empty-promises/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eThere\u0026rsquo;s an engineer I\u0026rsquo;ve worked with for nearly twenty years. He\u0026rsquo;s technically skilled, reasonably intelligent, often under pressure, and thoroughly convinced that Future Self will clean up whatever he leaves behind.\u003c/p\u003e\n\u003cp\u003eHis name is Past Self. He\u0026rsquo;s my arch enemy. And he writes all my oldest code.\u003c/p\u003e\n\u003cp\u003eThis is the second part of the \u003ca href=\"/posts/code-as-legacy/\"\u003e\u003cem\u003eCode as Legacy\u003c/em\u003e\u003c/a\u003e series. In part one, I made the case that code is a legacy (something you leave behind), and that the difference between a gift and a burden is almost entirely determined by how carefully it was built. This part is about what happens when you weren\u0026rsquo;t careful. About the person responsible. And about the uncomfortable realization that Past Self and Future Self are the same person, separated by time and context and the slow erosion of memory.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"past-self-characterized\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#past-self-characterized\" title=\"Past Self, Characterized\"\u003ePast Self, Characterized\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003ePast Self is not a villain. That\u0026rsquo;s the first thing to understand, and the most annoying one.\u003c/p\u003e\n\u003cp\u003eHe was usually working under real constraints: a deadline that wasn\u0026rsquo;t negotiable, a requirement that kept changing, a codebase he inherited and didn\u0026rsquo;t fully understand. He made the trade-offs that made sense at the time, with the information he had. I know this because I was there. I remember the Jira ticket. I remember the conversation that ended with \u0026ldquo;just get it working for now.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eWhat Past Self lacked wasn\u0026rsquo;t intelligence or intent. He lacked two things: imagination and humility.\u003c/p\u003e\n\u003cp\u003eHe couldn\u0026rsquo;t imagine that the code would still be running three years later in a context he\u0026rsquo;d never anticipated. And he wasn\u0026rsquo;t humble enough to admit, at the moment of the shortcut, that he was making a permanent decision while pretending it was temporary.\u003c/p\u003e\n\u003cp\u003eI know this because I still catch myself doing it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-evidence-file\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#the-evidence-file\" title=\"The Evidence File\"\u003eThe Evidence File\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEvery codebase I\u0026rsquo;ve worked on long enough has what I mentally call an evidence file: a collection of decisions Past Self made that Future Self is currently paying for. Here are a few entries from mine.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe connection string that became a foundation.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eEarly in a project, there was a SQL connection string in \u003ccode\u003eappsettings.json\u003c/code\u003e. Direct, clear, no abstraction. It worked. Nobody moved it when the project grew. Then it got referenced in six places. Then someone built a multi-tenancy feature that assumed a single database. Then we needed to support read replicas. By the time Future Self arrived at this problem, the connection string wasn\u0026rsquo;t a configuration value anymore. It was structural. Changing it meant touching half the service layer.\u003c/p\u003e\n\u003cp\u003ePast Self had forty seconds to introduce an abstraction. He didn\u0026rsquo;t, because \u0026ldquo;we\u0026rsquo;ll refactor when we need to.\u0026rdquo; Future Self needed two sprints.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe \u003ccode\u003ebool\u003c/code\u003e parameter that grew up.\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Past Self, six years ago\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eSendNotificationAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eisUrgent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eReasonable at the time. Two states, clear semantics. Then came \u0026ldquo;also high priority but not urgent,\u0026rdquo; then \u0026ldquo;urgent but silent,\u0026rdquo; then \u0026ldquo;urgent and high-priority and batched.\u0026rdquo; The method signature became:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Future Self, inheriting the mess\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eSendNotificationAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eisUrgent\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eisHighPriority\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eisSilent\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eisBatched\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFive booleans. All positional. All looking identical at every call site. All impossible to read without hovering over the method signature. Past Self\u0026rsquo;s \u003ccode\u003ebool\u003c/code\u003e was the reasonable starting point. The problem was that nobody stopped to redesign when it started multiplying:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// What Future Self eventually had to write anyway\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eSendNotificationAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eNotificationOptions\u003c/span\u003e \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eNotificationOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eNotificationPriority\u003c/span\u003e \u003cspan class=\"n\"\u003ePriority\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eNotificationPriority\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNormal\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eSilent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eBatched\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eenum\u003c/span\u003e \u003cspan class=\"n\"\u003eNotificationPriority\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eNormal\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eHigh\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUrgent\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis was always the right shape. Past Self just didn\u0026rsquo;t know it yet, and neither did I, when I was him.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe log statement that ate the disk.\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing order {OrderId}: {@Order}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e{@Order}\u003c/code\u003e serializes the entire object. Including the \u003ccode\u003eCustomer\u003c/code\u003e navigation property. Including the \u003ccode\u003eCustomer.Orders\u003c/code\u003e collection. Including each of those orders\u0026rsquo; \u003ccode\u003eCustomer\u003c/code\u003e navigation properties. On a Tuesday morning with normal traffic: fine. On Black Friday, with order volume at 40× normal: the logging pipeline wrote 800 MB of JSON per minute, filled the disk, and took down the service.\u003c/p\u003e\n\u003cp\u003ePast Self was debugging something. He wanted to see the full order object. He committed the log line and forgot it was there.\u003c/p\u003e\n\u003cp\u003eFuture Self found it during a post-mortem at 3 AM.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-you-cant-fire-past-self\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#why-you-cant-fire-past-self\" title=\"Why You Can\u0026rsquo;t Fire Past Self\"\u003eWhy You Can\u0026rsquo;t Fire Past Self\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe obvious response to all of this is: why didn\u0026rsquo;t you fix it at the time? Why didn\u0026rsquo;t you write it correctly from the start?\u003c/p\u003e\n\u003cp\u003eSometimes the answer is genuine negligence, and I won\u0026rsquo;t pretend otherwise. But more often, Past Self was operating under a set of conditions that made the decision locally rational even if it was globally wrong:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHe didn\u0026rsquo;t have the full picture.\u003c/strong\u003e The connection string was in \u003ccode\u003eappsettings.json\u003c/code\u003e because nobody had decided on a multi-tenancy strategy yet. The \u003ccode\u003ebool\u003c/code\u003e was \u003ccode\u003ebool\u003c/code\u003e because the requirements only described two states. Decisions that look obviously wrong in retrospect were made before the retrospect existed.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHe was optimizing for the wrong horizon.\u003c/strong\u003e Software development has strong incentives to ship now and a much weaker feedback loop for the cost of what you shipped. Past Self felt the deadline. He did not feel the two-sprint refactor that happened three years after he\u0026rsquo;d moved on to a different feature.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHe told himself it was temporary.\u003c/strong\u003e This is the one I find hardest to forgive, because it\u0026rsquo;s the most deliberate self-deception. \u0026ldquo;We\u0026rsquo;ll clean this up\u0026rdquo; is a phrase Past Self used as a get-out-of-jail card, knowing full well who would be holding the bill.\u003c/p\u003e\n\u003cp\u003eThat person is me. Future Self is not some abstract successor or a colleague who joins the team later. Future Self is me, roughly twelve months from now, with no memory of what I was thinking today, inheriting whatever I ship this week. He doesn\u0026rsquo;t get a briefing. He gets a diff.\u003c/p\u003e\n\u003cp\u003eYou can\u0026rsquo;t fire Past Self because he\u0026rsquo;s already gone. All you can do is clean up after him, try not to become him, and (this is the part that matters) think carefully about what you\u0026rsquo;re about to hand yourself.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-asymmetry-problem\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#the-asymmetry-problem\" title=\"The Asymmetry Problem\"\u003eThe Asymmetry Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what makes Past Self so dangerous: the cost of his decisions is borne entirely by Future Self.\u003c/p\u003e\n\u003cp\u003eThis asymmetry is not unique to software. It shows up everywhere that consequences are deferred: environmental policy, infrastructure maintenance, pension systems. The person who makes the decision and the person who lives with it are not the same person. This creates a systematic bias toward decisions that look good now and cost later.\u003c/p\u003e\n\u003cp\u003eIn software, the version of this that I see most often is what I\u0026rsquo;d call \u003cstrong\u003ethe invisible tax\u003c/strong\u003e. Past Self doesn\u0026rsquo;t add a line item to the budget for his shortcuts. He doesn\u0026rsquo;t log the future cost anywhere. Future Self just finds, gradually, that everything is harder than it should be. Features take longer. Bugs are more frequent. Changes in one place break things in unexpected places. Nobody points at a specific decision and calls it out, because Past Self\u0026rsquo;s decisions are distributed across thousands of lines of code, each one small and deniable, each one contributing to a codebase that resists change at every turn.\u003c/p\u003e\n\u003cp\u003eThe tax is real. It\u0026rsquo;s just invisible until you try to spend.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-future-self-deserves\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#what-future-self-deserves\" title=\"What Future Self Deserves\"\u003eWhat Future Self Deserves\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is the part Past Self consistently gets wrong: Future Self isn\u0026rsquo;t an abstraction. He\u0026rsquo;s me, a year from now, with no memory of what I was thinking today. He inherits my shortcuts the same way I inherited Past Self\u0026rsquo;s, not as a debt somebody else took on, but as his problem to solve with whatever time and energy he has left after dealing with everything else.\u003c/p\u003e\n\u003cp\u003eHe\u0026rsquo;ll find the code in the middle of something else. He\u0026rsquo;ll have thirty minutes to understand what I wrote and why, fix whatever broke, and get out without making it worse. He won\u0026rsquo;t have my context. He won\u0026rsquo;t have the Slack thread. He won\u0026rsquo;t have the meeting where I decided the timeout should be 30 seconds because the legacy service was slow and the client couldn\u0026rsquo;t wait for a proper fix.\u003c/p\u003e\n\u003cp\u003eWhat he deserves is code that doesn\u0026rsquo;t require archaeology to understand.\u003c/p\u003e\n\u003cp\u003eThis doesn\u0026rsquo;t mean over-documentation. It doesn\u0026rsquo;t mean exhaustive comments. It means code that makes its assumptions visible, surfaces its constraints, and fails clearly when something goes wrong. It means the difference between:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Past Self\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etimeout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e30000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eand:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Future Self can understand this without a Slack thread\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Matches the SLA of the legacy ReportService endpoint (see ADR-042)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003eReportGenerationTimeout\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOne is a magic number with no explanation. The other is a decision with enough context that Future Self can evaluate whether the constraint still applies, and change it if it doesn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eThe comment here is justified precisely because it encodes \u003cem\u003ewhy\u003c/em\u003e, not \u003cem\u003ewhat\u003c/em\u003e. The what is obvious. The why was in someone\u0026rsquo;s head, and now it isn\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-uncomfortable-continuity\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#the-uncomfortable-continuity\" title=\"The Uncomfortable Continuity\"\u003eThe Uncomfortable Continuity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve been writing about Past Self as if he\u0026rsquo;s a separate person. He isn\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eEvery piece of code I write today becomes part of Past Self\u0026rsquo;s legacy within the year. The shortcut I take this afternoon because the sprint ends on Friday will be Future Self\u0026rsquo;s archaeology project sometime in 2027. The \u003ccode\u003e// TODO: handle this properly\u003c/code\u003e I leave in because I\u0026rsquo;m tired becomes the thing that nobody ever comes back to fix.\u003c/p\u003e\n\u003cp\u003eThe uncomfortable truth is that Past Self is not a character from my past. He\u0026rsquo;s a character I\u0026rsquo;m actively writing right now: every time I ship something I know isn\u0026rsquo;t quite right, every time I leave a decision implicit that should be explicit, every time I tell myself Future Self will deal with it.\u003c/p\u003e\n\u003cp\u003eHe won\u0026rsquo;t deal with it. He\u0026rsquo;ll be too busy dealing with something else Past Self left behind.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-peace-without-excusing\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#making-peace-without-excusing\" title=\"Making Peace Without Excusing\"\u003eMaking Peace Without Excusing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve made peace with Past Self, more or less. Not because he didn\u0026rsquo;t cause damage. He did, measurably, in sprints and in incident hours and in engineers who got frustrated and left. But because the alternative to making peace is a kind of paralysis that doesn\u0026rsquo;t help anyone.\u003c/p\u003e\n\u003cp\u003eWhat I haven\u0026rsquo;t done is excuse him.\u003c/p\u003e\n\u003cp\u003eMaking peace means: I understand why you made those decisions. I understand the constraints, the pressure, the incomplete picture. I know you weren\u0026rsquo;t trying to create problems.\u003c/p\u003e\n\u003cp\u003eNot excusing means: you still should have known better on some of this. The magic numbers. The deferred decisions you knew were permanent. The \u003ccode\u003e// TODO\u003c/code\u003e comments you never intended to come back to. Those weren\u0026rsquo;t forced on you by constraints. Those were choices.\u003c/p\u003e\n\u003cp\u003eThe difference matters because excusing everything Past Self did means never learning anything from him. And making peace means I can look at the evidence file without anger, figure out what\u0026rsquo;s worth fixing and what isn\u0026rsquo;t, and move forward.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-im-handing-future-self\"\u003e\u003ca href=\"/posts/code-as-legacy-past-self/#what-im-handing-future-self\" title=\"What I\u0026rsquo;m Handing Future Self\"\u003eWhat I\u0026rsquo;m Handing Future Self\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s where I get to confess: this article is partly an accountability document.\u003c/p\u003e\n\u003cp\u003eI maintain systems that have Past Self\u0026rsquo;s fingerprints all over them. Some of it I\u0026rsquo;ve fixed. Some of it I\u0026rsquo;ve accepted as the cost of the original decisions. Some of it I\u0026rsquo;m actively making worse right now, probably, in ways I can\u0026rsquo;t see yet.\u003c/p\u003e\n\u003cp\u003eWhat I\u0026rsquo;m trying to do differently (and what I\u0026rsquo;d argue is the only practical response to the Past Self problem) is to make the implicit explicit, every time, even when it\u0026rsquo;s inconvenient. Not to write more code, but to write code that explains itself. To make the assumptions visible, the constraints documented, the failure modes clear.\u003c/p\u003e\n\u003cp\u003eFuture Self will still find things Past Self left behind. That\u0026rsquo;s inevitable. What I can control is whether Future Self finds them with enough context to understand what he\u0026rsquo;s looking at, or whether he has to figure it out from first principles at 3 AM while something is broken in production.\u003c/p\u003e\n\u003cp\u003eThe code I write today is a letter to myself: to someone who will have no idea what I was thinking, who will be under pressure, who will need to understand this quickly and get out cleanly. I know who Future Self is. I know what his days look like, because they look like mine.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;m trying to write him clearer letters.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eThis is part two of the \u003ca href=\"/posts/code-as-legacy/\"\u003eCode as Legacy\u003c/a\u003e series. Part one covers what \u0026ldquo;building carefully\u0026rdquo; actually means in practice.\u003c/em\u003e\u003c/p\u003e\n","date_modified":"2026-05-26T17:06:12+02:00","date_published":"2026-05-26T17:00:00+02:00","id":"https://daily-devops.net/posts/code-as-legacy-past-self/","language":"en","summary":"Past Self is the most dangerous engineer on your team: skilled, well-intentioned, and gone when the bill comes due. This is about the code he left behind.\n","tags":["softwareengineering","codequality","technicaldebt","architecture","dotnet","csharp","bestpractices"],"title":"My Biggest Enemy Writes My Code\n","url":"https://daily-devops.net/posts/code-as-legacy-past-self/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eI have spent half a day staring at a production incident wondering why I could not correlate any log entries across a single request. Everything looked fine. \u003ccode\u003eILogger\u003c/code\u003e was there, \u003ccode\u003eBeginScope\u003c/code\u003e was called, the structured properties were in the templates. In development, the console showed exactly what I expected. In production: nothing. No correlation ID. No scope context. Just a flat stream of messages from parallel requests, interleaved, undifferentiated, useless.\u003c/p\u003e\n\u003cp\u003eThe culprit was not a bug. It was me not understanding what \u003ccode\u003eILogger\u003c/code\u003e actually is: a façade with a lot of opt-in behaviour that looks enabled by default.\u003c/p\u003e\n\u003cp\u003eThat incident cost me half a day. I have seen variants of it in nearly every codebase I have worked in since. The patterns are always the same: a developer who trusts that logging works because it compiles, and finds out in production that it does not.\u003c/p\u003e\n\u003cp\u003eThis is a tour of the ways \u003ccode\u003eILogger\u003c/code\u003e lies to you, and by that I mean: the ways its defaults and abstractions let you believe things are working when they are not. These are not obscure edge cases. Most of them are the default configuration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-1-your-log-message-is-evaluated-before-the-level-check\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-1-your-log-message-is-evaluated-before-the-level-check\" title=\"Lie 1: Your Log Message Is Evaluated Before the Level Check\"\u003eLie 1: Your Log Message Is Evaluated Before the Level Check\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis one is easy to miss because it never crashes. It just silently costs you.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Processing order {order.Id} with {order.Items.Count} items totaling {order.Total:C}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf Debug is filtered out (which it is, in every default production configuration), the string interpolation still runs. \u003ccode\u003eorder.Items.Count\u003c/code\u003e is evaluated. The currency format is applied. Memory is allocated for the full interpolated string. Then the whole thing is thrown away.\u003c/p\u003e\n\u003cp\u003eYou will not notice this until you profile something. And then you will find Debug-level log calls in your hot path costing you measurable throughput, silently, in production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-message-templates-beat-interpolation\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#why-message-templates-beat-interpolation\" title=\"Why Message Templates Beat Interpolation\"\u003eWhy Message Templates Beat Interpolation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe fix is message templates, not because they are more readable, but because the parameters are not evaluated until after the level check:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing order {OrderId} with {ItemCount} items totaling {Total}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe structured fields are also preserved as separate properties rather than baked into a string, which matters for Lie 4.\u003c/p\u003e\n\u003cp\u003eFor anything called frequently: \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eLoggerMessage source generators\u003c/a\u003e. Zero allocation when filtered, correct property types, generated at compile time.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLogMessages\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [LoggerMessage(Level = LogLevel.Debug, Message = \u0026#34;Processing order {OrderId} with {ItemCount} items totaling {Total}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessingOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eitemCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003etotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is what \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e source generators exist for. If you are not using them in hot paths, you are paying for logging that is disabled.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-2-your-log-scopes-are-probably-not-appearing\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-2-your-log-scopes-are-probably-not-appearing\" title=\"Lie 2: Your Log Scopes Are Probably Not Appearing\"\u003eLie 2: Your Log Scopes Are Probably Not Appearing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is the one that cost me half a day.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginScope\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OrderId: {OrderId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Starting payment processing\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Sending confirmation email\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe premise is clean: every log call inside the \u003ccode\u003eusing\u003c/code\u003e block carries \u003ccode\u003eOrderId\u003c/code\u003e. When you have hundreds of parallel requests hitting a service, this is how you keep them separate in your logs. Without it, you get an undifferentiated stream where tracing a single request means grepping for an ID you may or may not have logged consistently.\u003c/p\u003e\n\u003cp\u003eIn development, the console shows the scope. You trust it. You ship it.\u003c/p\u003e\n\u003cp\u003eIn production, \u003ccode\u003eBeginScope\u003c/code\u003e returns a disposable that does nothing. No error. No warning. The scope is dropped entirely.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-beginscope-returns-a-no-op\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#why-beginscope-returns-a-no-op\" title=\"Why BeginScope Returns A No-Op\"\u003eWhy BeginScope Returns A No-Op\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe reason is that scope support is opt-in per provider. \u003ccode\u003eAddConsole()\u003c/code\u003e supports scopes but does not include them in output unless you enable it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;Logging\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;Console\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;IncludeScopes\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnd that is just the console. Every production sink (Application Insights, Seq, Elasticsearch) has its own scope configuration, its own opt-in. If your sink\u0026rsquo;s provider does not implement \u003ccode\u003eIExternalScopeConsumer\u003c/code\u003e, the \u003ccode\u003eSetScopeProvider\u003c/code\u003e call never happens, and every \u003ccode\u003eBeginScope\u003c/code\u003e you call is a no-op at the provider level.\u003c/p\u003e\n\u003cp\u003eI found this out by looking at the sink source code after half a day of adding increasingly desperate debug logging. The fix was one line in the sink configuration. The knowledge that I needed to add that line existed nowhere near the \u003ccode\u003eBeginScope\u003c/code\u003e documentation.\u003c/p\u003e\n\u003cp\u003eBefore you depend on scope data for incident correlation: query your actual production logs for a scope property. Verify it is there as a separate field, not missing entirely.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-3-your-minimum-level-configuration-has-contradictions-you-have-not-noticed\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-3-your-minimum-level-configuration-has-contradictions-you-have-not-noticed\" title=\"Lie 3: Your Minimum Level Configuration Has Contradictions You Have Not Noticed\"\u003eLie 3: Your Minimum Level Configuration Has Contradictions You Have Not Noticed\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe default \u003ccode\u003eappsettings.json\u003c/code\u003e logging section looks sane enough:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;Logging\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;LogLevel\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Default\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Information\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Microsoft\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Warning\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Microsoft.Hosting.Lifetime\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Information\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe behavior is a longest-prefix match. \u003ccode\u003eMicrosoft.Hosting.Lifetime\u003c/code\u003e beats \u003ccode\u003eMicrosoft\u003c/code\u003e because it is more specific. The order inside the configuration object is irrelevant. Fine.\u003c/p\u003e\n\u003cp\u003eNow add Serilog (which most production .NET applications do at some point):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;Serilog\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;MinimumLevel\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Default\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Information\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Override\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nt\"\u003e\u0026#34;Microsoft\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Warning\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYou now have two independent filter systems. Both must pass. Your \u003ccode\u003eMicrosoft.Hosting.Lifetime: Information\u003c/code\u003e override in \u003ccode\u003eLogging:LogLevel\u003c/code\u003e has no equivalent in Serilog, so Serilog\u0026rsquo;s \u003ccode\u003eMicrosoft: Warning\u003c/code\u003e blocks it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"useserilog-bypasses-your-loglevel-section\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#useserilog-bypasses-your-loglevel-section\" title=\"UseSerilog Bypasses Your LogLevel Section\"\u003eUseSerilog Bypasses Your LogLevel Section\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBut here is the part that genuinely surprised me: when you use \u003ccode\u003eUseSerilog()\u003c/code\u003e in your host setup, the \u003ccode\u003eLogging\u003c/code\u003e section in \u003ccode\u003eappsettings.json\u003c/code\u003e is bypassed entirely. Serilog replaces the entire MEL provider. Your \u003ccode\u003eLogLevel\u003c/code\u003e configuration (the one you have been editing, the one that looks like it should be in charge) is not read at all. Only the \u003ccode\u003eSerilog\u003c/code\u003e section matters.\u003c/p\u003e\n\u003cp\u003eI have seen people spend significant time adjusting the \u003ccode\u003eLogging:LogLevel\u003c/code\u003e configuration in a codebase where \u003ccode\u003eUseSerilog()\u003c/code\u003e was in \u003ccode\u003eProgram.cs\u003c/code\u003e. Every change had zero effect, and there was no indication why.\u003c/p\u003e\n\u003cp\u003ePick one authoritative minimum level configuration and remove the other. Do not maintain both.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-4-your-structured-properties-are-not-structured\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-4-your-structured-properties-are-not-structured\" title=\"Lie 4: Your Structured Properties Are Not Structured\"\u003eLie 4: Your Structured Properties Are Not Structured\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe whole point of \u003ccode\u003eILogger\u003c/code\u003e with message templates, over plain \u003ccode\u003eConsole.WriteLine\u003c/code\u003e, is that \u003ccode\u003e{OrderId}\u003c/code\u003e becomes a queryable property in your log aggregation system, not a substring buried in a flat string. That is the pitch for structured logging.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Payment failed for {CustomerId} with error {ErrorCode}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eerrorCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWith a correctly configured structured sink, you can query \u003ccode\u003eErrorCode == \u0026quot;INSUFFICIENT_FUNDS\u0026quot;\u003c/code\u003e. That query is indexed. It is fast. It works across millions of entries.\u003c/p\u003e\n\u003cp\u003eWith a plain text sink (or a structured sink with default formatting), you get \u003ccode\u003e\u0026quot;Payment failed for cust-123 with error INSUFFICIENT_FUNDS\u0026quot;\u003c/code\u003e. That is a string. You search it with a substring match. Under load, across millions of entries, you wait.\u003c/p\u003e\n\u003cp\u003eThree things must all be true for structured properties to actually be structured:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eYou use message templates, not string interpolation\u003c/li\u003e\n\u003cli\u003eYour sink supports structured output\u003c/li\u003e\n\u003cli\u003eYour sink is configured to output properties as separate fields\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThat third point is the one that bites you. Application Insights, by default, maps everything into \u003ccode\u003ecustomDimensions\u003c/code\u003e under a single composite key in some configurations. File sinks writing plain text give you a formatted message and nothing else. The console in default mode renders the template as a string.\u003c/p\u003e\n\u003cp\u003eThe practical test is simple: take a log entry from your production aggregation system that uses a structured parameter. Check whether the parameter appears as its own field. If it is embedded in the message string, your structured logging is decorative. It looks right in code and does nothing useful at query time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-5-exception-logging-loses-the-inner-exception-chain\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-5-exception-logging-loses-the-inner-exception-chain\" title=\"Lie 5: Exception Logging Loses the Inner Exception Chain\"\u003eLie 5: Exception Logging Loses the Inner Exception Chain\u003c/a\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order processing failed for {OrderId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is correct usage. \u003ccode\u003eILogger\u003c/code\u003e accepts an exception as the first parameter, serializes it, attaches it to the log event. You did everything right.\u003c/p\u003e\n\u003cp\u003eWhat you get in your logs depends entirely on what the sink does with \u003ccode\u003eException.ToString()\u003c/code\u003e versus a structured exception decomposition.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-aggregateexception-hides-the-real-failure\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#when-aggregateexception-hides-the-real-failure\" title=\"When AggregateException Hides The Real Failure\"\u003eWhen AggregateException Hides The Real Failure\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe problem is \u003ccode\u003eAggregateException\u003c/code\u003e and friends. \u003ccode\u003eAggregateException: One or more errors occurred.\u003c/code\u003e is the most useless log entry in the .NET ecosystem. It tells you exactly nothing about what actually failed. The real exception is in \u003ccode\u003eInnerException\u003c/code\u003e, one or more levels deep.\u003c/p\u003e\n\u003cp\u003eApplication Insights handles this well, serializing the full exception chain into separate telemetry entries. A plain JSON file sink with default settings often gives you just the outer exception type and message. You stare at \u003ccode\u003eAggregateException\u003c/code\u003e and go spelunking through stack traces.\u003c/p\u003e\n\u003cp\u003eIf you cannot configure your sink\u0026rsquo;s exception serialization depth, make the root cause explicit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einnermost\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInnerException\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInnerException\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order processing failed for {OrderId}. Root cause: {RootCause}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is defensive. It makes the useful information explicit in the log message rather than relying on the sink to dig it out of the exception chain.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-6-your-log-timestamps-are-potentially-wrong\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-6-your-log-timestamps-are-potentially-wrong\" title=\"Lie 6: Your Log Timestamps Are Potentially Wrong\"\u003eLie 6: Your Log Timestamps Are Potentially Wrong\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eILogger\u003c/code\u003e does not add timestamps. The sink does. The sink decides what \u0026ldquo;now\u0026rdquo; means and in which timezone it records it.\u003c/p\u003e\n\u003cp\u003eThree places this goes wrong:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUTC vs local time confusion.\u003c/strong\u003e Your application runs in UTC. Your sink records local time based on the server\u0026rsquo;s system clock. Your aggregation system converts to UTC. Depending on timezone offsets and Daylight Saving Time (DST), you end up with timestamps that are consistently wrong by hours. Correlating logs across services (one in UTC, one in local) means doing timezone arithmetic during an incident.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"clock-skew-across-services\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#clock-skew-across-services\" title=\"Clock Skew Across Services\"\u003eClock Skew Across Services\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eClock skew in distributed systems.\u003c/strong\u003e Multiple services writing to the same aggregation endpoint. Each server\u0026rsquo;s Network Time Protocol (NTP) sync is slightly different, maybe 50ms, maybe 500ms. Log entries that should be sequential appear out of order when sorted by timestamp. You lose the ability to reconstruct event sequences across service boundaries.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBuffered writes with stale timestamps.\u003c/strong\u003e Some sinks batch writes for throughput. The timestamp attached to the log event is the time of the sink write, not the time of the log call. Under load, that drift can be seconds. You cannot trust the timestamp order to represent the call order.\u003c/p\u003e\n\u003cp\u003eFor Serilog, be explicit about timestamp format and timezone:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eLoggerConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteTo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoutputTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e\u0026#34;[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] ...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnd verify that \u003ccode\u003eTimestamp\u003c/code\u003e is captured at call time, not write time. For buffered sinks, use a custom enricher to capture \u003ccode\u003eDateTimeOffset.UtcNow\u003c/code\u003e at the point the log method is called if ordering matters to you.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-actually-works\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#what-actually-works\" title=\"What Actually Works\"\u003eWhat Actually Works\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern across all six of these is the same: \u003ccode\u003eILogger\u003c/code\u003e compiles, runs, produces no errors, and silently does something different from what you expected. None of these are bugs. They are documentation you did not read, or opt-in behaviour that looks like a default.\u003c/p\u003e\n\u003cp\u003eI am not being harsh about Microsoft here — this is mostly a user problem. The documentation exists. The configuration options are there. But the defaults are not conservative defaults that fail loudly when misconfigured. They are optimistic defaults that look like they are working until you need them to actually work.\u003c/p\u003e\n\u003cp\u003eA correctly configured logging pipeline:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eUse message templates everywhere. No string interpolation in log calls.\u003c/li\u003e\n\u003cli\u003eUse \u003ccode\u003e[LoggerMessage]\u003c/code\u003e source generators for any log call in a hot path.\u003c/li\u003e\n\u003cli\u003eUse \u003ccode\u003eBeginScope\u003c/code\u003e for correlation context (request ID, user ID, operation ID) — and verify scope support for your specific sink in your specific configuration before you depend on it.\u003c/li\u003e\n\u003cli\u003eConfigure one authoritative minimum level source. Either \u003ccode\u003eLogging:LogLevel\u003c/code\u003e or Serilog\u0026rsquo;s \u003ccode\u003eMinimumLevel\u003c/code\u003e. Not both.\u003c/li\u003e\n\u003cli\u003eWrite at least one integration test that queries actual log output and verifies structured properties appear as separate fields, not embedded in message strings.\u003c/li\u003e\n\u003cli\u003eVerify exception serialization depth for your sink with a deliberately thrown \u003ccode\u003eAggregateException\u003c/code\u003e. Look at what you get.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe only way to know what your logs actually contain is to look at them in production, under real conditions. Development logging lies to you in the opposite direction — it shows you scopes, structured properties, and correct timestamps because the console provider actually works.\u003c/p\u003e\n\u003cp\u003eProduction is where the assumptions fail. Look there.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003eILogger\u003c/code\u003e is a façade over a pipeline you did not configure. The pipeline does not care that you trusted the façade.\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-05-21T17:00:00+02:00","id":"https://daily-devops.net/posts/your-ilogger-is-lying-to-you/","language":"en","summary":"Half a day lost to BeginScope silently doing nothing in production. ILogger compiles, runs, produces no errors, and fails quietly in six distinct ways.","tags":["logging","dotnet","csharp","observability","bestpractices","softwareengineering"],"title":"Six Ways ILogger Silently Fails in Production","url":"https://daily-devops.net/posts/your-ilogger-is-lying-to-you/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eMy author bio ends with a sentence I\u0026rsquo;ve been carrying for years:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eThe code you create is a valuable legacy, so it\u0026rsquo;s important to build it carefully.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIt sounds like something you\u0026rsquo;d frame and hang above a whiteboard. It isn\u0026rsquo;t. It\u0026rsquo;s the distilled result of watching systems survive their authors, outlive their requirements, and eventually become someone else\u0026rsquo;s problem — sometimes that someone else being me, years later, at 2 AM.\u003c/p\u003e\n\u003cp\u003eThis article is the story behind that sentence.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-legacy-actually-means\"\u003e\u003ca href=\"/posts/code-as-legacy/#what-legacy-actually-means\" title=\"What \u0026ldquo;Legacy\u0026rdquo; Actually Means\"\u003eWhat \u0026ldquo;Legacy\u0026rdquo; Actually Means\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe word legacy in software has been colonized by negativity. \u0026ldquo;Legacy system\u0026rdquo; means old, unmaintainable, the thing you inherited and wish you hadn\u0026rsquo;t. People say it like an apology.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not how I use it.\u003c/p\u003e\n\u003cp\u003eA legacy is what you leave behind. It can be a gift or a burden — and the difference is almost entirely determined by how carefully it was built. The Colosseum is a legacy. So is every \u003ccode\u003estatic readonly Dictionary\u0026lt;string, object\u0026gt;\u003c/code\u003e that someone thread-unsafe-cached against a singleton in 2014 and then shipped to production without tests.\u003c/p\u003e\n\u003cp\u003eBoth will outlast their creators. Only one will be admired.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-compounding-cost-of-carelessness\"\u003e\u003ca href=\"/posts/code-as-legacy/#the-compounding-cost-of-carelessness\" title=\"The Compounding Cost of Carelessness\"\u003eThe Compounding Cost of Carelessness\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIn nearly twenty years of .NET systems, the most expensive decisions I\u0026rsquo;ve witnessed weren\u0026rsquo;t made by incompetent people. They were made by skilled engineers in a hurry, under pressure, with incomplete context, who told themselves: \u003cem\u003e\u0026ldquo;We\u0026rsquo;ll clean this up later.\u0026rdquo;\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eLater never comes. Or rather, it comes in the form of an incident.\u003c/p\u003e\n\u003cp\u003eConsider what \u0026ldquo;building carefully\u0026rdquo; actually costs at the moment of creation:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eEnabling \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003enullable reference types\u003c/a\u003e in a new project: \u003cstrong\u003eminutes\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eEnabling them three years later across 200,000 lines: \u003cstrong\u003emonths\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eAdding an \u003ccode\u003e.editorconfig\u003c/code\u003e with analyzer rules at project start: \u003cstrong\u003eone afternoon\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eEnforcing consistency across an organic codebase after four teams touched it: \u003cstrong\u003ea quarter\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eWriting a proper \u003ccode\u003eCancellationToken\u003c/code\u003e propagation pattern from the start: \u003cstrong\u003etrivial\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eRetrofitting cancellation into an async call tree that never anticipated it: \u003cstrong\u003esurgical, risky, and slow\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"the-cancellation-token-you-should-have-added\"\u003e\u003ca href=\"/posts/code-as-legacy/#the-cancellation-token-you-should-have-added\" title=\"The Cancellation Token You Should Have Added\"\u003eThe Cancellation Token You Should Have Added\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe CancellationToken case is worth pausing on, because it\u0026rsquo;s so easy to defer and so expensive when you do. A call tree without cancellation looks harmless:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_orderRepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetOrdersAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003einvoices\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_invoiceRepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetInvoicesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epdf\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_pdfService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRenderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorders\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einvoices\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epdf\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eA year later, HTTP timeouts fire while the PDF renderer keeps allocating and the database queries keep running — because there\u0026rsquo;s nothing to stop them. Retrofitting cancellation now means touching every signature in the chain, every interface, every test, every caller. Versus what \u0026ldquo;careful at creation time\u0026rdquo; looked like:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ect\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_orderRepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetOrdersAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ect\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003einvoices\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_invoiceRepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetInvoicesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ect\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epdf\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_pdfService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRenderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorders\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einvoices\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ect\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epdf\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOne parameter. Thirty seconds. That\u0026rsquo;s the decision that was \u0026ldquo;not needed yet.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis is not a coincidence. It is compounding interest on technical debt, and the interest rate is not linear. The further the decision recedes into the past, the more the code has grown around it, the harder it is to reach, and the more things break when you try.\u003c/p\u003e\n\u003cp\u003eCareful building is cheap. Careless building is cheap too — until it isn\u0026rsquo;t. And it always stops being cheap at the worst possible moment.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-carefully-does-not-mean\"\u003e\u003ca href=\"/posts/code-as-legacy/#what-carefully-does-not-mean\" title=\"What \u0026ldquo;Carefully\u0026rdquo; Does Not Mean\"\u003eWhat \u0026ldquo;Carefully\u0026rdquo; Does Not Mean\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve made a mistake I see others repeat: confusing \u0026ldquo;carefully\u0026rdquo; with \u0026ldquo;perfectly.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003ePerfectly is a trap. It produces over-engineered systems that look impeccable in architecture diagrams and are misery to extend. I have taken over projects from consultants who preached Clean Code and delivered something that could not change without collapsing. Everything was carefully named, carefully layered, carefully documented — and completely rigid.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not careful. That\u0026rsquo;s fearful.\u003c/p\u003e\n\u003cp\u003eCareful means four things — none of them perfectionism.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUnderstanding the operating costs of what you write.\u003c/strong\u003e A \u003ccode\u003eDictionary\u003c/code\u003e is not thread-safe. An \u003ccode\u003easync void\u003c/code\u003e swallows exceptions silently. A \u003ccode\u003eGuid.NewGuid()\u003c/code\u003e primary key fragments your index with every insert. Not obscure knowledge — basic operating costs that change the failure mode of code that otherwise compiles and ships fine. \u003ccode\u003easync void\u003c/code\u003e is the instructive one: exceptions escape unobserved, hit the thread pool, and crash the process with no stack trace pointing back to the source:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// async void: exception becomes unobservable noise\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eOnMessageReceived\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eobject\u003c/span\u003e \u003cspan class=\"n\"\u003esender\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageEventArgs\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessMessageAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// async Task: caller can catch, log, and handle\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eOnMessageReceivedAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eobject\u003c/span\u003e \u003cspan class=\"n\"\u003esender\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageEventArgs\u003c/span\u003e \u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessMessageAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eGuid\u003c/code\u003e case is slower-burning. Both versions below ship on day one. The difference shows up in production monitoring three months later, when you notice your index is 60% fragmented and inserts are taking four times longer than they should:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e \u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNewGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// random, causes page splits\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e \u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateVersion7\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// monotonically increasing, .NET 9+ (see: https://learn.microsoft.com/en-us/dotnet/api/system.guid.createversion7)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"writing-for-the-next-reader\"\u003e\u003ca href=\"/posts/code-as-legacy/#writing-for-the-next-reader\" title=\"Writing For The Next Reader\"\u003eWriting For The Next Reader\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eOptimizing for the reader, not the writer.\u003c/strong\u003e The next person to read this code is often you, six months from now, with no memory of what you were thinking. Deliberate code — code that makes its assumptions visible — is not slower to write. It\u0026rsquo;s more expensive to start and cheaper to maintain forever after.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eKnowing when good enough actually is good enough.\u003c/strong\u003e Careful is not exhaustive. Configuration loaded once at startup does not need nanosecond optimization. A nightly batch job does not need payment-processor reliability. Misapplied care creates its own form of debt — rigidity dressed up as quality.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"making-assumptions-visible-to-the-compiler\"\u003e\u003ca href=\"/posts/code-as-legacy/#making-assumptions-visible-to-the-compiler\" title=\"Making Assumptions Visible To The Compiler\"\u003eMaking Assumptions Visible To The Compiler\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eMaking the implicit explicit.\u003c/strong\u003e The most dangerous code in any system isn\u0026rsquo;t complex code — it\u0026rsquo;s code where critical assumptions live in someone\u0026rsquo;s head instead of in the type system or the tests. The two implementations below are functionally equivalent on a happy path. Only one survives a new developer joining the team:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// assumptions in the developer\u0026#39;s head\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eInvoiceService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_taxRates\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eFormatAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eregionId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e$\u0026#34;{amount * _taxRates[regionId]:C}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// assumptions in the compiler\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eInvoiceService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIReadOnlyDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_taxRates\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eInvoiceService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIReadOnlyDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003etaxRates\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_taxRates\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etaxRates\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etaxRates\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eFormatAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eregionId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003e_taxRates\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eregionId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003erate\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eKeyNotFoundException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;No tax rate configured for region {regionId}.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;{amount * rate:C}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe second version is longer because it encodes what was previously undocumented: tax rates are required, null is not acceptable, and an unknown region is a programming error — not a silent zero that produces a wrong invoice.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"code-outlives-context\"\u003e\u003ca href=\"/posts/code-as-legacy/#code-outlives-context\" title=\"Code Outlives Context\"\u003eCode Outlives Context\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere is the thing that took me the longest to internalize:\u003c/p\u003e\n\u003cp\u003eThe context in which you wrote the code will not survive. The business requirement that made the trade-off obvious will be forgotten. The pressure that justified the shortcut will be invisible. The Slack thread explaining why the timeout is hardcoded to 30 seconds will scroll into history. The team that understood the design will disperse.\u003c/p\u003e\n\u003cp\u003eWhat remains is the code.\u003c/p\u003e\n\u003cp\u003eAnd someone will have to work with it without your context, your justifications, or your intentions. They will read what you wrote and form conclusions. They will extend it, debug it, and curse it — or understand it and be grateful.\u003c/p\u003e\n\u003cp\u003eThat is the legacy.\u003c/p\u003e\n\u003cp\u003eI have been both recipients. I\u0026rsquo;ve inherited systems where everything was explained by what the code did — where reading a class told you not just how it worked but why, what it was protecting against, and where the landmines were. I\u0026rsquo;ve also inherited systems that required six months of archaeology before I trusted any change I made.\u003c/p\u003e\n\u003cp\u003eThe engineers who wrote both kinds were equally intelligent. The difference was care.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-relationship-between-care-and-speed\"\u003e\u003ca href=\"/posts/code-as-legacy/#the-relationship-between-care-and-speed\" title=\"The Relationship Between Care and Speed\"\u003eThe Relationship Between Care and Speed\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTeams that haven\u0026rsquo;t experienced this tension believe that careful code is slower to produce than careless code. They\u0026rsquo;re right in the short term. A quick hack ships faster than a considered design — once.\u003c/p\u003e\n\u003cp\u003eWhat they miss is the asymmetry in the other direction.\u003c/p\u003e\n\u003cp\u003eCareless code is expensive to extend, expensive to debug, expensive to test, expensive to hand off, and expensive to explain. Every future interaction with that code costs more than it needed to. The total cost of ownership grows with the number of future interactions, and production code has a lot of future interactions.\u003c/p\u003e\n\u003cp\u003eCareful code costs more upfront and less every time after.\u003c/p\u003e\n\u003cp\u003eThis is not an abstract economic argument. I can point to specific decisions in systems I maintain where five minutes of thinking at creation time would have saved months of debugging over the lifetime of the feature. I can also point to the opposite: careful designs that held up under four years of changing requirements without needing to be rewritten.\u003c/p\u003e\n\u003cp\u003eThe careful code was not slower to develop. It was \u003cstrong\u003eslower to start and faster to finish\u003c/strong\u003e — across the entire lifecycle of the feature.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-i-actually-do-differently\"\u003e\u003ca href=\"/posts/code-as-legacy/#what-i-actually-do-differently\" title=\"What I Actually Do Differently\"\u003eWhat I Actually Do Differently\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAfter nearly twenty years, \u0026ldquo;build carefully\u0026rdquo; has specific practices attached to it. These are not aspirational principles. They are the concrete things I do, or insist my teams do, because I\u0026rsquo;ve felt the cost of not doing them.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEnable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e from day zero.\u003c/strong\u003e Not as a code review substitute — as a safety net that operates at compilation time. I configure them in \u003ccode\u003e.editorconfig\u003c/code\u003e at project creation, severity-as-error for the things that matter, and when they produce noise I fix the noise rather than silence the rule. The six rules I never start a project without:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[*.cs]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA2007.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003eerror   # ConfigureAwait missing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1031.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003ewarning # catch Exception (too broad)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1051.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003eerror   # public instance fields\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1825.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003eerror   # unnecessary array allocation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CS8600.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003eerror   # nullable dereference\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CS8602.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003eerror   # possible null reference\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThese rules catch bugs that appear in incident reports, not in code review — which is exactly the point.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"forcing-yourself-to-articulate-intent\"\u003e\u003ca href=\"/posts/code-as-legacy/#forcing-yourself-to-articulate-intent\" title=\"Forcing Yourself To Articulate Intent\"\u003eForcing Yourself To Articulate Intent\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eWrite the summary before the method.\u003c/strong\u003e Not a docstring — a sentence in my head: \u003cem\u003e\u0026ldquo;This method does X and assumes Y.\u0026rdquo;\u003c/em\u003e If I can\u0026rsquo;t complete that sentence clearly, I don\u0026rsquo;t understand my own code well enough to ship it. This sounds trivial. It isn\u0026rsquo;t. It catches underspecified designs before they become permanent.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTreat \u003ccode\u003eTODO\u003c/code\u003e comments as deferred decisions, not reminders.\u003c/strong\u003e Every \u003ccode\u003e// TODO: fix this properly\u003c/code\u003e is a piece of context that will expire. Either I fix it now, create a tracked issue with enough context that a stranger could complete it, or I accept that it will never be fixed and stop pretending otherwise. The lie that \u0026ldquo;we\u0026rsquo;ll come back to this\u0026rdquo; is one of the most expensive fictions in software.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-pre-commit-diff-review-habit\"\u003e\u003ca href=\"/posts/code-as-legacy/#the-pre-commit-diff-review-habit\" title=\"The Pre-commit Diff Review Habit\"\u003eThe Pre-commit Diff Review Habit\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eRead the diff before every commit.\u003c/strong\u003e Not to catch typos — to notice surprises. If I see code I don\u0026rsquo;t remember writing or can\u0026rsquo;t explain, that\u0026rsquo;s the signal. Familiar code that suddenly looks strange is often code that shouldn\u0026rsquo;t be committed yet.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eName things for what they are, not what they do.\u003c/strong\u003e \u003ccode\u003eCustomerRepository\u003c/code\u003e tells you the mechanism. \u003ccode\u003eCustomerAccess\u003c/code\u003e is vague. \u003ccode\u003eActiveCustomersByRegionQuery\u003c/code\u003e tells you what you\u0026rsquo;re getting and why. The noun matters. The qualifier matters more.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-longer-arc\"\u003e\u003ca href=\"/posts/code-as-legacy/#the-longer-arc\" title=\"The Longer Arc\"\u003eThe Longer Arc\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI carry that motto in my bio because it is the most honest thing I can say about why I write the way I write and build the way I build.\u003c/p\u003e\n\u003cp\u003eIt isn\u0026rsquo;t about perfectionism. It isn\u0026rsquo;t about impressing code reviewers or following the fashionable methodology of the moment. It\u0026rsquo;s about the relationship between present decisions and future consequences — and taking that relationship seriously enough to slow down slightly, every single time, and ask: \u003cem\u003e\u0026ldquo;Is this how I would want to find this?\u0026rdquo;\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eMost of the time, the answer is no. That\u0026rsquo;s fine. That\u0026rsquo;s the question working.\u003c/p\u003e\n\u003cp\u003eThe code you write today will be maintained by someone who doesn\u0026rsquo;t know what you were thinking. It might be a colleague. It might be a future version of yourself. It might be someone you\u0026rsquo;ll never meet, building on a library you published and forgot about.\u003c/p\u003e\n\u003cp\u003eDo them the courtesy of building it carefully.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-05-19T17:00:00+02:00","id":"https://daily-devops.net/posts/code-as-legacy/","language":"en","summary":"Code is not just something you write—it is something you leave behind. After nearly two decades in production, here is what treating code as legacy means.\n","tags":["softwareengineering","codequality","bestpractices","technicaldebt","architecture","dotnet","csharp"],"title":"The Code You Write Today Is Someone's Problem Tomorrow\n","url":"https://daily-devops.net/posts/code-as-legacy/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eThere is a class of bugs that only appear on the last day of the month. Or when a session expires at exactly midnight. Or when a scheduled job runs at 23:59 and the next run lands in the previous day\u0026rsquo;s bucket. Or when the daylight savings transition eats a token that was perfectly valid an hour ago. These bugs have one thing in common: time was hardcoded, and nobody thought to test it.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eDateTime.UtcNow\u003c/code\u003e is not a neutral utility call. It is a hidden dependency: one that couples your logic to the real wall clock, makes deterministic testing impossible, and silently produces bugs that only manifest in production at the worst possible moment. You cannot reproduce them on your laptop. You cannot write a unit test that catches them. You ship them and wait.\u003c/p\u003e\n\u003cp\u003e.NET 8 shipped \u003ccode\u003eTimeProvider\u003c/code\u003e in November 2023. It is an official abstraction for time in the .NET runtime, backed by Microsoft, available in the \u003ccode\u003eSystem\u003c/code\u003e namespace with no extra packages. It exists specifically to solve this problem. It is not experimental. It is not a preview. It is stable, documented, and ships with the runtime.\u003c/p\u003e\n\u003cp\u003eTwo years later, most codebases I encounter have never heard of it. Some have heard of it and decided to deal with it later. Later has not arrived.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-with-datetimeutcnow\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#the-problem-with-datetimeutcnow\" title=\"The Problem With DateTime.UtcNow\"\u003eThe Problem With DateTime.UtcNow\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a typical token expiry check:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eIsTokenExpired\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eissuedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003evalidity\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eissuedAt\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003evalidity\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis looks correct. It is untestable.\u003c/p\u003e\n\u003cp\u003eTo write a test that verifies tokens expire after 15 minutes, you either:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePass a token issued 15 minutes ago and depend on the real clock running forward (flaky)\u003c/li\u003e\n\u003cli\u003eIntroduce a \u003ccode\u003eFunc\u0026lt;DateTime\u0026gt;\u003c/code\u003e parameter and pass \u003ccode\u003e() =\u0026gt; DateTime.UtcNow\u003c/code\u003e in production (informal workaround)\u003c/li\u003e\n\u003cli\u003eWrap \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e in your own \u003ccode\u003eIClock\u003c/code\u003e interface (reinventing the wheel every project)\u003c/li\u003e\n\u003cli\u003eSkip the test and hope it works in production (common)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEvery team arrives at one of these approaches independently. They all work around the same missing abstraction.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-timeprovider-is\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#what-timeprovider-is\" title=\"What TimeProvider Is\"\u003eWhat TimeProvider Is\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eTimeProvider\u003c/code\u003e is an abstract class in the \u003ccode\u003eSystem\u003c/code\u003e namespace, available from .NET 8. For .NET 6 and .NET 7 you can install the \u003ca href=\"https://www.nuget.org/packages/Microsoft.Bcl.TimeProvider\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eMicrosoft.Bcl.TimeProvider\u003c/code\u003e\u003c/a\u003e NuGet package to get the same API. The API surface is deliberately small:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eabstract\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eTimeProvider\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e \u003cspan class=\"n\"\u003eSystem\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTimeOffset\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTimeOffset\u003c/span\u003e \u003cspan class=\"n\"\u003eGetLocalNow\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeZoneInfo\u003c/span\u003e \u003cspan class=\"n\"\u003eLocalTimeZone\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"kt\"\u003elong\u003c/span\u003e \u003cspan class=\"n\"\u003eGetTimestamp\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"kt\"\u003elong\u003c/span\u003e \u003cspan class=\"n\"\u003eTimestampFrequency\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"n\"\u003eITimer\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateTimer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimerCallback\u003c/span\u003e \u003cspan class=\"n\"\u003ecallback\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject?\u003c/span\u003e \u003cspan class=\"n\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003edueTime\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003eperiod\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eTimeProvider.System\u003c/code\u003e is the real implementation. It delegates to the system clock. You inject it in production, replace it in tests.\u003c/p\u003e\n\u003cp\u003eThe less obvious part: \u003ccode\u003eTimeProvider\u003c/code\u003e is not just a \u003ccode\u003eDateTime\u003c/code\u003e wrapper. It also controls \u003ccode\u003eITimer\u003c/code\u003e creation, which means periodic timers and cancellation token timeouts become testable without any threading tricks.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rewriting-the-example\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#rewriting-the-example\" title=\"Rewriting the Example\"\u003eRewriting the Example\u003c/a\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eTokenValidator\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e \u003cspan class=\"n\"\u003e_time\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTokenValidator\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e \u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_time\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eIsTokenExpired\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDateTimeOffset\u003c/span\u003e \u003cspan class=\"n\"\u003eissuedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003evalidity\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_time\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eissuedAt\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003evalidity\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eProduction registration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddSingleton\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat is the entire change for production code. One line in \u003ccode\u003eProgram.cs\u003c/code\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"testing-with-faketimeprovider\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#testing-with-faketimeprovider\" title=\"Testing With FakeTimeProvider\"\u003eTesting With FakeTimeProvider\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft ships \u003ccode\u003eFakeTimeProvider\u003c/code\u003e in the \u003ccode\u003eMicrosoft.Extensions.TimeProvider.Testing\u003c/code\u003e package:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eFakeTimeProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTimeOffset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2024\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eZero\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003evalidator\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTokenValidator\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eissuedAt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eAddMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(-\u003c/span\u003e\u003cspan class=\"m\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsTokenExpired\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eissuedAt\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNo threading. No \u003ccode\u003eThread.Sleep\u003c/code\u003e. No flaky timing windows. Deterministic, instant, readable.\u003c/p\u003e\n\u003cp\u003eYou can also advance time explicitly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdvance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is particularly valuable for testing scenarios where time advances during a sequence of operations: session renewal, retry backoff, lease expiry, scheduled job windowing.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-timer-problem-nobody-mentions\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#the-timer-problem-nobody-mentions\" title=\"The Timer Problem Nobody Mentions\"\u003eThe Timer Problem Nobody Mentions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eDateTime.UtcNow\u003c/code\u003e gets most of the attention, but \u003ccode\u003eTimeProvider\u003c/code\u003e solves a harder problem: controlled timers.\u003c/p\u003e\n\u003cp\u003eConsider a retry policy with exponential backoff:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRetryAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e \u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eattempt\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003eattempt\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003eattempt\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ecatch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edelay\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eMath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePow\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eattempt\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edelay\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateCancellationTokenSource\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edelay\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"advancing-time-without-real-waits\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#advancing-time-without-real-waits\" title=\"Advancing Time Without Real Waits\"\u003eAdvancing Time Without Real Waits\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWith \u003ccode\u003eFakeTimeProvider\u003c/code\u003e, you can advance time programmatically to trigger the delay without actually waiting:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eFakeTimeProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eretryTask\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRetryAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efailingOperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdvance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// trigger first retry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdvance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// trigger second retry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efakeTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdvance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e4\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// trigger third retry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eretryTask\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTesting retry logic without real waits. No \u003ccode\u003eTask.Delay(100)\u003c/code\u003e hacks in tests, no thread sleep, no 30-second test suites.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-already-uses-timeprovider\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#what-already-uses-timeprovider\" title=\"What Already Uses TimeProvider\"\u003eWhat Already Uses TimeProvider\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe .NET runtime itself migrated key components to \u003ccode\u003eTimeProvider\u003c/code\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eCancellationTokenSource(TimeSpan)\u003c/code\u003e: accepts a \u003ccode\u003eTimeProvider\u003c/code\u003e constructor overload\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePeriodicTimer\u003c/code\u003e: controllable via \u003ccode\u003eFakeTimeProvider\u003c/code\u003e when time is advanced\u003c/li\u003e\n\u003cli\u003eCancellation-based delays: make waits testable by passing \u003ccode\u003etimeProvider.CreateCancellationTokenSource(delay).Token\u003c/code\u003e to \u003ccode\u003eTask.Delay\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIf you use any of these in tested code and still use \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e directly, you have inconsistent time abstraction in the same codebase.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-iclock-pattern-is-dead\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#the-iclock-pattern-is-dead\" title=\"The IClock Pattern Is Dead\"\u003eThe IClock Pattern Is Dead\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMany .NET codebases I have worked in roll their own \u003ccode\u003eIClock\u003c/code\u003e or \u003ccode\u003eISystemClock\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eIClock\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eSystemClock\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIClock\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis pattern works. It has worked for years. But from .NET 8 onward it is redundant. \u003ccode\u003eTimeProvider\u003c/code\u003e is the platform-standardized version of exactly this interface. Running both side by side means:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTwo abstractions for the same thing\u003c/li\u003e\n\u003cli\u003eTests need to know which one a class uses\u003c/li\u003e\n\u003cli\u003eNew team members implement it a third way\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe correct migration path: replace \u003ccode\u003eIClock\u003c/code\u003e with \u003ccode\u003eTimeProvider\u003c/code\u003e. They are structurally equivalent; the migration is mostly mechanical.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-microsoft-deprecated-isystemclock\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#why-microsoft-deprecated-isystemclock\" title=\"Why Microsoft Deprecated ISystemClock\"\u003eWhy Microsoft Deprecated ISystemClock\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eASP.NET Core\u0026rsquo;s own \u003ccode\u003eISystemClock\u003c/code\u003e was deprecated in .NET 8 in favor of \u003ccode\u003eTimeProvider\u003c/code\u003e. If Microsoft deprecated their own version, the signal is clear.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-you-cannot-inject-timeprovider\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#when-you-cannot-inject-timeprovider\" title=\"When You Cannot Inject TimeProvider\"\u003eWhen You Cannot Inject TimeProvider\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSometimes you cannot easily restructure the class to accept \u003ccode\u003eTimeProvider\u003c/code\u003e via constructor injection (legacy code, sealed classes, static methods). In these cases:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eTimeContext\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ThreadStatic]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003e_current\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e \u003cspan class=\"n\"\u003eCurrent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eget\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_current\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eset\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_current\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSet \u003ccode\u003eTimeContext.Current\u003c/code\u003e to a \u003ccode\u003eFakeTimeProvider\u003c/code\u003e at test setup, reset it in teardown. Not as clean as injection, but eliminates the hidden \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e dependency without full restructuring.\u003c/p\u003e\n\u003cp\u003eThis is a migration aid, not a target architecture. Prefer injection.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-one-rule\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#the-one-rule\" title=\"The One Rule\"\u003eThe One Rule\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAnywhere you write \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e, \u003ccode\u003eDateTime.Now\u003c/code\u003e, or \u003ccode\u003eDateTimeOffset.UtcNow\u003c/code\u003e in code that will be tested: inject \u003ccode\u003eTimeProvider\u003c/code\u003e instead.\u003c/p\u003e\n\u003cp\u003eThat is the entire rule. The surface area is smaller than you think. Most \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e calls cluster in a handful of classes: token validators, session managers, audit loggers, scheduled job coordinators. Migrate those and you have covered 90% of the problem.\u003c/p\u003e\n\u003cp\u003eThe remaining 10% is simple timestamp annotations for \u0026ldquo;created at\u0026rdquo; or display formatting. Those do not need controllable time. Leave them alone.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eYou cannot test what you cannot control. Time is not special. Abstract it.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"start-monday-not-next-quarter\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#start-monday-not-next-quarter\" title=\"Start Monday, Not Next Quarter\"\u003eStart Monday, Not Next Quarter\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere is the practical adoption path for an existing codebase:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAdd \u003ccode\u003eMicrosoft.Extensions.TimeProvider.Testing\u003c/code\u003e as a test project dependency\u003c/li\u003e\n\u003cli\u003eRegister \u003ccode\u003eTimeProvider.System\u003c/code\u003e in your dependency injection (DI) container: \u003ccode\u003eservices.AddSingleton(TimeProvider.System);\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eSearch for \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e, \u003ccode\u003eDateTime.Now\u003c/code\u003e, and \u003ccode\u003eDateTimeOffset.UtcNow\u003c/code\u003e across the codebase\u003c/li\u003e\n\u003cli\u003eIdentify the classes with the most time-sensitive logic: token validation, session management, audit logging, scheduling\u003c/li\u003e\n\u003cli\u003eRefactor those classes to accept \u003ccode\u003eTimeProvider\u003c/code\u003e via constructor injection\u003c/li\u003e\n\u003cli\u003eWrite deterministic tests using \u003ccode\u003eFakeTimeProvider\u003c/code\u003e for every scenario that previously required timing hacks or was simply skipped\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFor a medium-sized codebase, this is a focused half-day of work. The payoff is permanent.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-teams-resist-the-migration\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#why-teams-resist-the-migration\" title=\"Why Teams Resist The Migration\"\u003eWhy Teams Resist The Migration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTeams that resist this change usually land on one of two positions. The first: \u0026ldquo;our codebase doesn\u0026rsquo;t have time-related bugs.\u0026rdquo; Almost certainly it does. Those bugs surface on the last day of the month, during a daylight savings transition, or when a scheduled job runs at 23:58 and the next one lands in a different day\u0026rsquo;s bucket. They are waiting. The second position: \u0026ldquo;the refactor is too risky.\u0026rdquo; Changing four constructors to accept an additional parameter is not risky. Shipping a session expiry mechanism that cannot be tested is risky.\u003c/p\u003e\n\u003cp\u003eThere is also a subtler concern worth naming: teams that have lived with \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e for years have normalized the absence of time-related tests. When there is no mechanism to freeze the clock, you stop writing tests that require a frozen clock. The problem becomes invisible. \u003ccode\u003eTimeProvider\u003c/code\u003e does not just improve testability; it forces the question of which time-sensitive code is actually tested at all. That question tends to have uncomfortable answers.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eTimeProvider\u003c/code\u003e is not a premature abstraction. It is the correction of a design oversight that has existed since .NET Framework 1.0. The system clock was always the wrong model for code that needs to behave deterministically in tests. The ecosystem simply lacked a sanctioned, stable alternative until .NET 8.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-platform-has-already-moved\"\u003e\u003ca href=\"/posts/stop-pretending-timeprovider-doesnt-exist/#the-platform-has-already-moved\" title=\"The Platform Has Already Moved\"\u003eThe Platform Has Already Moved\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft has made the direction clear: \u003ccode\u003eISystemClock\u003c/code\u003e in ASP.NET Core is deprecated, the runtime migrated its own timer-based APIs, and the testing support ships in the official Microsoft NuGet feed. The platform has moved. The question is whether your codebase catches up before the next production incident where time was the hidden variable nobody thought to test.\u003c/p\u003e\n\u003cp\u003eAbstract your time dependencies. Test the scenarios you cannot reproduce manually. Ship fewer midnight bugs.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-05-14T17:00:00+02:00","id":"https://daily-devops.net/posts/stop-pretending-timeprovider-doesnt-exist/","language":"en","summary":"DateTime.UtcNow is a hidden dependency that breaks tests at midnight. .NET 8 shipped TimeProvider in 2023; two years on, most codebases still ignore it.","tags":["testing","dotnet","csharp","bestpractices","softwareengineering"],"title":"Stop Pretending TimeProvider Doesn't Exist","url":"https://daily-devops.net/posts/stop-pretending-timeprovider-doesnt-exist/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eGitHub Copilot code review is available in pull requests. Claude can review a diff. Cursor highlights issues as you type. Every major AI coding assistant now offers some form of review, and teams are using these tools to supplement (or in some cases replace) asynchronous human review on pull requests.\u003c/p\u003e\n\u003cp\u003eThis is not necessarily wrong. AI code review is genuinely useful. But there is a pattern to what it misses, and understanding that pattern matters more than debating whether to use these tools at all.\u003c/p\u003e\n\u003cp\u003eIn my experience, AI code reviewers behave like sycophants. They are good at finding small problems with how you built something. They are almost incapable of questioning whether you should have built it at all.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-ai-code-review-is-good-at\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#what-ai-code-review-is-good-at\" title=\"What AI Code Review Is Good At\"\u003eWhat AI Code Review Is Good At\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTo be clear: these tools are useful. Worth adding to your PR workflow.\u003c/p\u003e\n\u003cp\u003eAI reviews reliably catch:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eObvious bugs in isolation.\u003c/strong\u003e Null dereferences, off-by-one errors, incorrect operator precedence, missing \u003ccode\u003eawait\u003c/code\u003e, unchecked return values from methods that can fail. These are the bugs human reviewers also catch, and they slip through when reviewers are tired, rushed, or staring at a 500-line diff.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCommon anti-patterns.\u003c/strong\u003e \u003ccode\u003easync void\u003c/code\u003e, catching \u003ccode\u003eException\u003c/code\u003e without rethrowing, \u003ccode\u003eDateTime.Now\u003c/code\u003e instead of \u003ccode\u003eDateTime.UtcNow\u003c/code\u003e, string concatenation in loops, \u003ccode\u003eConfigureAwait(false)\u003c/code\u003e missing in library code. Pattern matching against known bad patterns is exactly what Large Language Models (LLMs) do well.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTrivial security issues.\u003c/strong\u003e SQL injection via string concatenation, hardcoded credentials, insecure random number generation. These appear in training data thousands of times.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStyle consistency.\u003c/strong\u003e Naming inconsistencies, missing XML documentation, inconsistent error handling patterns relative to the rest of the file.\u003c/p\u003e\n\u003cp\u003eThese categories represent real value. A review pass that catches these before human review means human reviewers can spend their time on harder problems.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-ai-code-review-systematically-misses\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#what-ai-code-review-systematically-misses\" title=\"What AI Code Review Systematically Misses\"\u003eWhat AI Code Review Systematically Misses\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is where the sycophancy shows up.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWrong abstraction.\u003c/strong\u003e AI reviewers evaluate the code you wrote against its own internal logic. They rarely notice that the abstraction itself is wrong: that the \u003ccode\u003eOrderProcessor\u003c/code\u003e class is doing three different things and probably should not exist as a single class, that the interface design couples callers to implementation details, that the naming reveals a confused mental model of the domain. Recognizing a wrong abstraction requires understanding the system it lives in and the cost of fixing it later. AI reviewers do not have that context.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;This should be deleted.\u0026rdquo;\u003c/strong\u003e The correct review comment for a surprising fraction of pull requests is something like: \u0026ldquo;This feature was not the right call, let\u0026rsquo;s talk before merging.\u0026rdquo; AI reviewers will not write that comment. They review code on its own terms. A well-implemented feature that solves the wrong problem gets a positive AI review, and that feedback loop, repeated over time, shapes how a team thinks about what quality means.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSystemic patterns across the codebase.\u003c/strong\u003e AI reviewers see the diff. They do not know that the same abstraction appeared in three other places and was wrong each time. They do not know that this exact approach was tried and reverted eight months ago, and that the revert commit explains why. Reviewers with codebase history catch this. AI reviewers cannot.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBusiness logic correctness.\u003c/strong\u003e Is this the right formula for calculating the surcharge? Does this authorization check correctly represent the access control model? Is this state machine transition valid given how the domain actually works? AI reviewers can tell you the code is internally consistent. They cannot tell you it is correct relative to what the software is supposed to do. This is not a minor gap. Business logic bugs are often the costliest bugs, and they are invisible to a reviewer that does not understand the business.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance under real load.\u003c/strong\u003e AI reviewers flag obvious O(n²) algorithms and missing database indexes in toy examples. They rarely have visibility into the data distribution, the access patterns, or the production load profile that determines whether the code will hold up at scale. The performance review that matters happens in load testing and production, not in the diff view.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-sycophancy-problem\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#the-sycophancy-problem\" title=\"The Sycophancy Problem\"\u003eThe Sycophancy Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe specific failure mode of AI code review is not that it misses things. Every review process misses things. The problem is the pattern of what it misses.\u003c/p\u003e\n\u003cp\u003eAI reviewers tend to approve the overall approach and find issues in the details. When a team leans heavily on AI review, there is a subtle risk: reviewers get better and better at fixing the details an AI flags, while the bigger structural questions get less attention over time. I have seen this happen, and it is not anyone\u0026rsquo;s fault. It is a natural response to the feedback signal you are getting.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-the-approval-bias-is-structural\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#why-the-approval-bias-is-structural\" title=\"Why The Approval Bias Is Structural\"\u003eWhy The Approval Bias Is Structural\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe approval bias is structural. AI reviewers are trained on review data where most code in a diff is acceptable. The kind of feedback that says \u0026ldquo;the entire approach here is wrong, close this PR and start over\u0026rdquo; is rare in training data and produces outcomes that make the tool seem less useful. So the model optimizes away from it.\u003c/p\u003e\n\u003cp\u003eThe result: AI reviewers are systematically biased toward approving what you built and suggesting small improvements. They are not calibrated to recognize when the correct response is rejection.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-confidence-effect-on-developers\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#the-confidence-effect-on-developers\" title=\"The Confidence Effect On Developers\"\u003eThe Confidence Effect On Developers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThere is also a confidence effect worth naming. A developer who ships a PR with zero AI findings tends to feel more confident that the code is solid. That confidence is not entirely wrong (the mechanical issues are likely clean), but it can crowd out the instinct to ask for a second human opinion. Over time, \u0026ldquo;the AI found nothing\u0026rdquo; starts to function as a substitute for \u0026ldquo;this is good code\u0026rdquo;, and that is a different claim entirely.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-ai-review-should-change-about-human-review\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#what-ai-review-should-change-about-human-review\" title=\"What AI Review Should Change About Human Review\"\u003eWhat AI Review Should Change About Human Review\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf AI review is in your pipeline, it should shift what human reviewers focus on, not replace them.\u003c/p\u003e\n\u003cp\u003eAI reviewers handle the mechanical layer well: obvious bugs, pattern violations, style issues. That creates an opportunity for human reviewers to focus on what AI cannot do:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIs this the right design?\u003c/li\u003e\n\u003cli\u003eDoes this code belong here at all?\u003c/li\u003e\n\u003cli\u003eDoes the naming suggest the author has a clear mental model of the domain?\u003c/li\u003e\n\u003cli\u003eIs this consistent with decisions made elsewhere in the system?\u003c/li\u003e\n\u003cli\u003eWhat will maintaining this cost in six months?\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"where-human-review-time-belongs\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#where-human-review-time-belongs\" title=\"Where Human Review Time Belongs\"\u003eWhere Human Review Time Belongs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHuman review time is finite. If a human reviewer spends twenty minutes on a PR that an AI already reviewed and only surfaces style issues, something has gone wrong with how review time is being used. The value of human review is judgment, context, and the willingness to say \u0026ldquo;not yet.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eA team that uses AI review to reduce the need for human judgment does not end up with less review. It ends up with coverage that feels high but catches less of what actually matters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-diff-problem\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#the-diff-problem\" title=\"The Diff Problem\"\u003eThe Diff Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBoth AI and human review share a structural limitation: they evaluate changes, not outcomes.\u003c/p\u003e\n\u003cp\u003eA large refactor that genuinely improves a design looks messy as a diff: deletions everywhere, moved code, renamed concepts. A small change that introduces a subtle bug can look perfectly clean. Both human and AI reviewers are influenced by the shape of the change, not just its effect on the codebase.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-ai-cannot-step-outside-the-diff\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#why-ai-cannot-step-outside-the-diff\" title=\"Why AI Cannot Step Outside The Diff\"\u003eWhy AI Cannot Step Outside The Diff\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI reviewers are more constrained here because they have no option to go beyond the diff. A human reviewer can pull the branch, run it, read the surrounding code, check git history. AI reviewers are limited to what is presented to them.\u003c/p\u003e\n\u003cp\u003eThis means AI review is structurally better suited to focused, contained changes, and less suited to catching problems that only become visible when you look at the broader context.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"a-concrete-library-migration-example\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#a-concrete-library-migration-example\" title=\"A Concrete Library Migration Example\"\u003eA Concrete Library Migration Example\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA concrete example: a PR that migrates a service to use a new internal library might look straightforward in the diff. The imports change, a few method calls are updated, tests pass. An AI reviewer sees nothing alarming. But a human who knows that the new library has different error propagation semantics, or that the migration breaks an assumption made elsewhere in the codebase, can catch that. The diff does not surface it. Context does.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"using-ai-review-without-becoming-dependent-on-it\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#using-ai-review-without-becoming-dependent-on-it\" title=\"Using AI Review Without Becoming Dependent on It\"\u003eUsing AI Review Without Becoming Dependent on It\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eA few practices that have worked well in my experience:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUse AI review as a pre-filter, not a gatekeeper.\u003c/strong\u003e Let it catch mechanical issues before human review. Humans then review for judgment, not syntax. An AI approval should not substitute for human review on anything that carries real risk.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTreat AI approval as a weak signal.\u003c/strong\u003e An AI saying \u0026ldquo;looks good\u0026rdquo; means it did not find a pattern match for common issues. That is useful information, but it is not an endorsement of the design.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRead what the AI flagged, and what it did not.\u003c/strong\u003e If it found nothing interesting, that is not evidence the code is good. It may mean the problems are exactly the kind the AI cannot see.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eKeep humans in the design conversation.\u003c/strong\u003e Architecture decisions, new abstractions, changes to domain models: these all need human review from someone with context. No AI reviewer carries your system\u0026rsquo;s history, your domain knowledge, or the judgment to tell you a design direction is off before you build it out.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWatch for approval drift.\u003c/strong\u003e If PRs consistently get AI approval and human reviewers gradually stop questioning design decisions, that is a signal worth paying attention to. The human review may have been quietly degraded, not supplemented.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-honest-summary\"\u003e\u003ca href=\"/posts/ai-code-review-is-a-sycophant/#the-honest-summary\" title=\"The Honest Summary\"\u003eThe Honest Summary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAI code review tools are useful. Add them to your pipeline. Let them handle the mechanical layer.\u003c/p\u003e\n\u003cp\u003eBut they are not reviewers in the sense that actually matters. They do not have judgment. They do not know your system. They cannot tell you that you built the wrong thing. They are pattern matchers with a structural bias toward approving what you wrote.\u003c/p\u003e\n\u003cp\u003eThe risk is not that these tools make developers worse (most developers using AI review are thoughtful professionals who also get human review). The risk is subtler: over time, optimizing for what AI review catches can quietly shift attention away from the questions it cannot ask. Staying aware of that dynamic is enough to avoid it.\u003c/p\u003e\n\u003cp\u003eAI review is a useful tool. Keep it in that category.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAn AI reviewer that says \u0026ldquo;looks good\u0026rdquo; is not telling you the code is good. It is telling you it did not find a match.\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-05-25T22:27:03+02:00","date_published":"2026-05-12T17:00:00+02:00","id":"https://daily-devops.net/posts/ai-code-review-is-a-sycophant/","language":"en","summary":"Copilot and Claude find real bugs, but miss wrong abstractions and bad designs. Understanding that gap matters more than debating the tools.","tags":["ai","softwareengineering","codequality","bestpractices","devops"],"title":"AI Code Review Is a Sycophant: Why It Always Approves","url":"https://daily-devops.net/posts/ai-code-review-is-a-sycophant/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYou add a NuGet package. Build time jumps from 2 seconds to 8. You rebuild a second time: still 8 seconds. You change one line of code: 8 seconds again. The package description said nothing about this. You just quietly accepted a 300% tax on every build for the rest of the project\u0026rsquo;s life.\u003c/p\u003e\n\u003cp\u003eThat package ships a source generator.\u003c/p\u003e\n\u003cp\u003eSource generators are one of the most powerful additions to the .NET compiler platform. They are also one of the most invisible performance costs in modern .NET development. Everyone writes about what they can do. Nobody writes about what they cost.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-source-generators-actually-do-on-every-build\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#what-source-generators-actually-do-on-every-build\" title=\"What Source Generators Actually Do (On Every Build)\"\u003eWhat Source Generators Actually Do (On Every Build)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe mental model most developers have: source generators run once, generate some code, done. That is wrong.\u003c/p\u003e\n\u003cp\u003eSource generators run as part of the Roslyn compilation pipeline. Every time you build (full build, incremental build, background build triggered by saving a file), every registered generator runs. Not optionally. Not conditionally. Every time.\u003c/p\u003e\n\u003cp\u003eA typical mid-sized .NET solution in 2025 has more active source generators than you think. Add them up:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003ePackage\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eGenerator\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eLoggerMessageGenerator\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eSystem.Text.Json\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eJsonSerializerSourceGenerator\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eAutoMapper.Extensions.Microsoft.DependencyInjection\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMapping generator\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eMapperly\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMapper generator\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eMicrosoft.NET.Sdk.Maui\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMultiple generators\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAny DI framework\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRegistration generator\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAny gRPC tooling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eService/client generator\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eEight to twelve active generators per project is not unusual. Each one is a Roslyn plugin executing against your full syntax tree on every build.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-two-kinds-of-source-generators\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#the-two-kinds-of-source-generators\" title=\"The Two Kinds of Source Generators\"\u003eThe Two Kinds of Source Generators\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNot all source generators are created equal. This distinction matters enormously for build performance.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003eISourceGenerator\u003c/code\u003e\u003c/strong\u003e: the original API from .NET 5. Receives the full compilation. Runs completely on every build. No caching, no incremental logic. If you have one of these, you pay full price every time regardless of what changed.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003eIIncrementalGenerator\u003c/code\u003e\u003c/strong\u003e: introduced in .NET 6. Uses a pipeline model that tracks which inputs actually changed. If your code change does not affect the generator\u0026rsquo;s inputs, the generator produces cached output and skips real work. Used correctly, incremental generators approach zero cost on unchanged code.\u003c/p\u003e\n\u003cp\u003eThe catch: many popular NuGet packages still ship \u003ccode\u003eISourceGenerator\u003c/code\u003e implementations. The API is not deprecated. There is no warning when you install a non-incremental generator. You find out at build time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"measuring-the-damage\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#measuring-the-damage\" title=\"Measuring the Damage\"\u003eMeasuring the Damage\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou cannot fix what you cannot measure. Fortunately, MSBuild gives you everything you need.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"binary-log\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#binary-log\" title=\"Binary Log\"\u003eBinary Log\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet build -bl\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis produces \u003ccode\u003emsbuild.binlog\u003c/code\u003e in your project directory. Open it with \u003ca href=\"https://msbuildlog.com/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMSBuild Structured Log Viewer\u003c/a\u003e. Search for \u003ccode\u003eGeneratorDriver\u003c/code\u003e or \u003ccode\u003eRunGenerators\u003c/code\u003e. You will see each generator, its execution time, and how often it ran.\u003c/p\u003e\n\u003cp\u003eA real example from a project I worked on:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-txt\" data-lang=\"txt\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRunGenerators (net9.0)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├── JsonSerializerSourceGenerator    42ms\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├── LoggerMessageGenerator            8ms\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├── MapperlyGenerator               890ms   ← problem\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  └── AutoMapperGenerator             340ms   ← problem\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat is 1.2 seconds per build from two mapping generators. At 300 builds per day (realistic for an active developer with file-save triggers), that is 6 minutes of daily waste. Per developer. On a 5-person team: 30 minutes every day.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"roslyn-generator-timing\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#roslyn-generator-timing\" title=\"Roslyn Generator Timing\"\u003eRoslyn Generator Timing\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor finer granularity, set the \u003ccode\u003eReportAnalyzer\u003c/code\u003e property:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;ReportAnalyzer\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/ReportAnalyzer\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBuild output will include per-generator timing in milliseconds. Slower and less detailed than binlog, but useful for quick checks without installing additional tools.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"multi-targeting-multiplies-everything\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#multi-targeting-multiplies-everything\" title=\"Multi-Targeting Multiplies Everything\"\u003eMulti-Targeting Multiplies Everything\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere is the cost multiplier nobody mentions in the documentation:\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;TargetFrameworks\u0026gt;\u003c/span\u003enet8.0;net9.0\u003cspan class=\"nt\"\u003e\u0026lt;/TargetFrameworks\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEvery source generator runs once per target framework. Two targets: double the cost. Three targets: triple. That MapperlyGenerator eating 890ms? Now it costs 1,780ms on every build. Every registered generator, every target, every time.\u003c/p\u003e\n\u003cp\u003eLibrary authors supporting \u003ccode\u003enet6.0;net7.0;net8.0;net9.0\u003c/code\u003e and shipping a non-incremental generator are imposing a 4× multiplier on every consumer of their package.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"hot-reload-the-silent-incompatibility\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#hot-reload-the-silent-incompatibility\" title=\"Hot Reload: The Silent Incompatibility\"\u003eHot Reload: The Silent Incompatibility\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSource generators and \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e.NET Hot Reload\u003c/a\u003e have a complicated relationship.\u003c/p\u003e\n\u003cp\u003eHot Reload works by applying incremental changes to a running process without a full restart. Source generators complicate this because the generated code might need to change when your code changes, and the Hot Reload mechanism cannot always determine whether that is safe.\u003c/p\u003e\n\u003cp\u003eThe result: if your project has source generators that interact with the code you changed, Hot Reload silently falls back to a full rebuild and restart. The \u003ccode\u003edotnet watch\u003c/code\u003e output tells you this happened:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-txt\" data-lang=\"txt\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewarn: Hot reload of changes succeeded but some changes required application restart.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYou lose the instant feedback loop that makes Hot Reload valuable. With several active generators, you may find Hot Reload effectively never works for your use case.\u003c/p\u003e\n\u003cp\u003eTo diagnose which generators break Hot Reload, temporarily remove them one by one and observe whether \u003ccode\u003edotnet watch\u003c/code\u003e starts applying changes without restart.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"ide-latency-the-intellisense-tax\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#ide-latency-the-intellisense-tax\" title=\"IDE Latency: The IntelliSense Tax\"\u003eIDE Latency: The IntelliSense Tax\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSource generators run in the background inside Visual Studio and Rider to keep generated code available for IntelliSense, navigation, and error highlighting. This is a continuous background process, not just a build-time concern.\u003c/p\u003e\n\u003cp\u003eNon-incremental generators re-run whenever the IDE detects a change to your syntax tree. Type a character, save a file, the generator chain kicks off. If your generators are slow, you experience this as:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIntelliSense suggestions appearing late or disappearing temporarily\u003c/li\u003e\n\u003cli\u003e\u0026ldquo;Analyzing\u0026hellip;\u0026rdquo; spinners that block navigation\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eGo to Definition\u003c/code\u003e jumping to stale generated code\u003c/li\u003e\n\u003cli\u003eIntermittent red squiggles on valid code\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is hard to attribute directly because IDEs do not surface per-generator timings in their UI. The binlog approach does not help here either since that measures CLI builds. Your best signal is disabling generators one at a time via \u003ccode\u003e\u0026lt;Analyzer Remove=\u0026quot;...\u0026quot; /\u0026gt;\u003c/code\u003e and observing whether IDE responsiveness improves.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-source-generators-are-worth-it\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#when-source-generators-are-worth-it\" title=\"When Source Generators Are Worth It\"\u003eWhen Source Generators Are Worth It\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSource generators are not the problem. The problem is using them without understanding the trade-off.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eClearly worth it:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e[LoggerMessage]\u003c/code\u003e source generator: eliminates allocation on every log call, compiler-enforced message templates. The runtime savings at high throughput far outweigh the build cost.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eSystem.Text.Json\u003c/code\u003e source generator: AOT-compatible serialization, zero reflection at runtime. Required for Native AOT scenarios, significant throughput improvement in hot paths.\u003c/li\u003e\n\u003cli\u003eStrongly-typed ID generators (like \u003ca href=\"https://github.com/andrewlock/StronglyTypedId\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eStronglyTypedId\u003c/code\u003e\u003c/a\u003e): compile-time correctness guarantee, zero runtime cost.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eOften not worth it:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eMapping generators for simple DTOs where a hand-written mapper takes 20 lines and compiles instantly\u003c/li\u003e\n\u003cli\u003eDI registration generators that save writing \u003ccode\u003eservices.AddScoped\u0026lt;IFoo, Foo\u0026gt;()\u003c/code\u003e a few times\u003c/li\u003e\n\u003cli\u003eBoilerplate generators for code that changes rarely and where T4 templates or a one-time script would suffice\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe deciding question: does this generator eliminate runtime cost, enforce correctness at compile time, or enable something impossible without it? If the answer is \u0026ldquo;it saves me from writing some repetitive code,\u0026rdquo; a T4 template or a code snippet achieves the same result without touching every build.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"how-to-check-whether-a-generator-is-incremental\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#how-to-check-whether-a-generator-is-incremental\" title=\"How to Check Whether a Generator Is Incremental\"\u003eHow to Check Whether a Generator Is Incremental\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBefore adding a package that ships a source generator, check whether its generator implements \u003ccode\u003eIIncrementalGenerator\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eThe fast way: look at the package\u0026rsquo;s GitHub repository and search for \u003ccode\u003eIIncrementalGenerator\u003c/code\u003e vs \u003ccode\u003eISourceGenerator\u003c/code\u003e. If the generator implements \u003ccode\u003eISourceGenerator\u003c/code\u003e, it is non-incremental.\u003c/p\u003e\n\u003cp\u003eThe programmatic way: inspect the assembly directly.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Reflection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eassembly\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eAssembly\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLoadFrom\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;path/to/generator.dll\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003egeneratorTypes\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eassembly\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003et\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetInterfaces\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFullName\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Microsoft.CodeAnalysis.ISourceGenerator\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e               \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFullName\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Microsoft.CodeAnalysis.IIncrementalGenerator\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etype\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003egeneratorTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eisIncremental\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetInterfaces\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFullName\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Microsoft.CodeAnalysis.IIncrementalGenerator\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{type.Name}: {(isIncremental ? \u0026#34;\u003c/span\u003e\u003cspan class=\"n\"\u003eIncremental\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34; : \u0026#34;\u003c/span\u003e\u003cspan class=\"n\"\u003eNon\u003c/span\u003e\u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eincremental\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;)}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf the generator is non-incremental and the package is popular, check whether there is an open issue or PR for the migration. Many maintainers have not made the switch simply because nobody asked.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"mitigation-strategies\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#mitigation-strategies\" title=\"Mitigation Strategies\"\u003eMitigation Strategies\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhen you cannot remove a generator but need to reduce its cost:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEmit and cache generated files:\u003c/strong\u003e\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;EmitCompilerGeneratedFiles\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/EmitCompilerGeneratedFiles\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;CompilerGeneratedFilesOutputPath\u0026gt;\u003c/span\u003e$(BaseIntermediateOutputPath)Generated\u003cspan class=\"nt\"\u003e\u0026lt;/CompilerGeneratedFilesOutputPath\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis writes generated files to disk. You can commit them to source control and exclude the generator from CI builds when inputs have not changed. Adds complexity; worth it for slow generators on large projects.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIsolate generators to dedicated projects:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eSplit your solution so that the code triggering expensive generators lives in a dedicated project that changes rarely. The generator only runs when that project rebuilds, not on every change to your main project.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDisable generators in specific configurations:\u003c/strong\u003e\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\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(Configuration)\u0026#39; == \u0026#39;Debug\u0026#39;\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;Analyzer\u003c/span\u003e \u003cspan class=\"na\"\u003eRemove=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;@(Analyzer)\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;%(Filename)\u0026#39; == \u0026#39;SlowGenerator\u0026#39;\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\u003eUse this sparingly. It can cause the Debug build to diverge from Release in ways that mask real errors.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eProfile before optimizing:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eMeasure with binlog first. The generator you suspect is slow is often not the actual problem. The one you never thought about frequently is.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bigger-picture\"\u003e\u003ca href=\"/posts/dotnet-source-generators-hidden-costs/#the-bigger-picture\" title=\"The Bigger Picture\"\u003eThe Bigger Picture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSource generators sit at the intersection of a real tension in modern .NET: zero runtime cost requires paying that cost somewhere else, and \u0026ldquo;somewhere else\u0026rdquo; is build time and IDE responsiveness.\u003c/p\u003e\n\u003cp\u003eThe .NET ecosystem has moved fast on source generators since .NET 5. The migration from \u003ccode\u003eISourceGenerator\u003c/code\u003e to \u003ccode\u003eIIncrementalGenerator\u003c/code\u003e is ongoing but incomplete. Many widely-used packages still ship non-incremental generators because the migration requires significant effort and the existing API works.\u003c/p\u003e\n\u003cp\u003eAs a consumer, the tools are available to you. Measure with binlog. Understand whether each generator pays its way. Push back on packages that impose non-incremental generators for convenience features.\u003c/p\u003e\n\u003cp\u003eThe build time you save is your own.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eProfile first. \u003cstrong\u003eThen optimize.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-05-07T17:00:00+02:00","id":"https://daily-devops.net/posts/dotnet-source-generators-hidden-costs/","language":"en","summary":"You added a NuGet package and your build jumped from 2 to 8 seconds. That package ships a source generator. Here is what it costs and how to find out.","tags":["sourcegenerators","dotnet","performance","csharp","bestpractices","softwareengineering"],"title":"Source Generators: The Build Performance Killer","url":"https://daily-devops.net/posts/dotnet-source-generators-hidden-costs/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour password reset endpoint probably returns the user\u0026rsquo;s full profile, purchase history, and marketing preferences. The caller asked for an email address. You gave them everything.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s purpose drift: exposing data beyond what the caller needs for its stated function. GDPR Article 5(1)(b) and ISO/IEC 27701 both require that personal data be processed only for the purpose it was collected. This isn\u0026rsquo;t a compliance checkbox. It\u0026rsquo;s an architectural constraint that determines how you structure API endpoints, implement authorization policies, and design response payloads.\u003c/p\u003e\n\u003cp\u003eMost .NET applications fail this test because they design APIs around database entities rather than caller purposes. The typical \u003ccode\u003eGetUser\u003c/code\u003e endpoint returns everything the database contains, regardless of why the caller requested it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-entity-centric-api-design\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#the-fatal-pattern-entity-centric-api-design\" title=\"The Fatal Pattern: Entity-Centric API Design\"\u003eThe Fatal Pattern: Entity-Centric API Design\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe typical ASP.NET Core API exposes personal data through broad entity endpoints:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[HttpGet(\u0026#34;{id}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserDto\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProfile\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddresses\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentMethods\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderHistory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefaultAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Any authenticated user can retrieve full data for any user ID\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eMapToDto\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis pattern violates purpose limitation in multiple ways:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"three-ways-entity-endpoints-leak-data\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#three-ways-entity-endpoints-leak-data\" title=\"Three Ways Entity Endpoints Leak Data\"\u003eThree Ways Entity Endpoints Leak Data\u003c/a\u003e\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eOver-exposure\u003c/strong\u003e: A dashboard widget showing \u0026ldquo;Welcome, John\u0026rdquo; receives the same payload as an order fulfillment system processing a shipment. The caller\u0026rsquo;s purpose doesn\u0026rsquo;t affect what data they receive.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo access controls\u003c/strong\u003e: Any authenticated user can retrieve full profile data for any user ID by incrementing the endpoint parameter. Authentication proves identity, but nothing validates purpose.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eInvisible data flows\u003c/strong\u003e: The API documentation describes endpoints but doesn\u0026rsquo;t specify why each field is returned. Developers consuming the API cannot determine whether their intended use complies with the original collection purpose.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFrom a .NET implementation perspective, this design treats \u003ccode\u003eUser\u003c/code\u003e as a data structure rather than a protected resource with context-dependent visibility.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"purpose-specific-endpoint-design\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#purpose-specific-endpoint-design\" title=\"Purpose-Specific Endpoint Design\"\u003ePurpose-Specific Endpoint Design\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEffective purpose limitation requires restructuring APIs around caller purposes rather than database entities:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[ApiController]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Route(\u0026#34;api/users\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserProfileController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuthorizationService\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e \u003cspan class=\"n\"\u003e_userService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Purpose: Display user greeting in application header\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Data Minimization: First name only\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpGet(\u0026#34;{id}/greeting\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUserGreetingDto\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetUserGreeting\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_userService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserByIdAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorizeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ViewOwnProfile\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eForbid\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUserGreetingDto\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Purpose: Account settings management\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Data Minimization: Profile fields only, excludes order/payment history\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpGet(\u0026#34;{id}/profile-settings\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Authorize]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProfileSettingsDto\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetProfileSettings\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_userService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserByIdAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eNotFound\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authorizationService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorizeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ManageOwnProfile\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eauthResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eForbid\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eProfileSettingsDto\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEmail\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eLastName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLastName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ePhoneNumber\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePhoneNumber\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe key difference: each endpoint documents its purpose and returns only the fields needed for that purpose. The greeting endpoint returns a first name. The profile settings endpoint returns contact information. Neither returns payment methods or order history because those fields serve different purposes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"resource-based-authorization-for-field-level-control\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#resource-based-authorization-for-field-level-control\" title=\"Resource-Based Authorization for Field-Level Control\"\u003eResource-Based Authorization for Field-Level Control\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eASP.NET Core\u0026rsquo;s resource-based authorization enables purpose-aware access control by evaluating both the user\u0026rsquo;s identity and the specific resource being accessed:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Authorization requirement that validates purpose context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeLimitationRequirement\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuthorizationRequirement\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eenum\u003c/span\u003e \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAccountManagement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderFulfillment\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCustomerSupport\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eSecurityMonitoring\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Authorization handler that checks consent for purpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeLimitationHandler\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eAuthorizationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIConsentService\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econsentService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleRequirementAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAuthorizationHandlerContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e \u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUser\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserIdClaim\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserIdClaim\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserIdClaim\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// Invalid or missing user identifier - deny access\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Users can always access their own data for account management\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAccountManagement\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// For other purposes, verify explicit consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehasConsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHasConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eConsentPurposeFromDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehasConsent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurposeFromDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerSupport\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerSupport\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurityMonitoring\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eConsentPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSecurityMonitoring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eInvalidOperationException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;No consent mapping for {purpose}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration in Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddScoped\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIAuthorizationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAuthorization\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequirements\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAccountManagement\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;MarketingAnalytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003epolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequirements\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationRequirement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDataProcessingPurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMarketingAnalytics\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis authorization handler enforces purpose limitation at the framework level. An endpoint attempting to access user data for marketing analytics will be denied unless the user explicitly consented to that purpose—even if the same data would be accessible for account management purposes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"field-level-authorization-with-graphql\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#field-level-authorization-with-graphql\" title=\"Field-Level Authorization with GraphQL\"\u003eField-Level Authorization with GraphQL\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eGraphQL APIs pose unique purpose limitation challenges because clients construct queries dynamically, requesting arbitrary field combinations. A query for \u003ccode\u003e{ user { email } }\u003c/code\u003e serves a different purpose than \u003ccode\u003e{ user { email orderHistory { items total } } }\u003c/code\u003e, but traditional authorization sees both as \u0026ldquo;get user\u0026rdquo; requests.\u003c/p\u003e\n\u003cp\u003eHotChocolate, the most comprehensive .NET GraphQL server, supports field-level authorization that enforces purpose limitation for each requested field:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserType\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eObjectType\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprotected\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigure\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIObjectTypeDescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Basic authentication required\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: account functionality\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBirthDate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;MarketingAnalytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: demographic analysis\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderHistory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OrderFulfillment\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: order processing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edescriptor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eField\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eu\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eu\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentMethods\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAuthorize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;PaymentProcessing\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Purpose: payment handling\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// GraphQL query with field-level validation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Query: { user(id: 123) { email birthDate orderHistory { total } } }\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Result: Returns email (account management), denies birthDate (no marketing consent),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e//         denies orderHistory (no order fulfillment permission)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;data\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;email\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;john@example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;birthDate\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Field denied - insufficient consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;orderHistory\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Field denied - insufficient permission\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;errors\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;message\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;The current user is not authorized to access this resource.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;path\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;birthDate\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;message\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;The current user is not authorized to access this resource.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;path\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;orderHistory\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"how-field-denials-surface-to-clients\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#how-field-denials-surface-to-clients\" title=\"How Field Denials Surface To Clients\"\u003eHow Field Denials Surface To Clients\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEach field specifies the processing purpose required to access it. A user query for demographic analysis receives \u003ccode\u003ebirthDate\u003c/code\u003e only if marketing analytics consent exists. An order processing query receives \u003ccode\u003eorderHistory\u003c/code\u003e only if the caller has order fulfillment permissions. The same user resource returns different field sets depending on the caller\u0026rsquo;s stated purpose.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"consent-aware-middleware-for-audit-context\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#consent-aware-middleware-for-audit-context\" title=\"Consent-Aware Middleware for Audit Context\"\u003eConsent-Aware Middleware for Audit Context\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003ePurpose limitation requires not just access control but audit trails documenting why data was accessed. Custom middleware captures the processing purpose for every request:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeAuditMiddleware\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuditLogger\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeAuditMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eRequestDelegate\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuditLogger\u003c/span\u003e \u003cspan class=\"n\"\u003eauditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_next\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_auditLogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eauditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Extract purpose from route or header\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Header approach allows clients to explicitly declare purpose\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Route-based fallback provides default behavior\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;X-Processing-Purpose\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e].\u003c/span\u003e\u003cspan class=\"n\"\u003eFirstOrDefault\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eDeterminePurposeFromRoute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Store in HttpContext for downstream authorization\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ProcessingPurpose\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Capture audit data before request\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eauditEntry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDataAccessAuditEntry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTimestamp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirst\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)?.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTargetUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eExtractTargetUserIdFromPath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEndpoint\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eIpAddress\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Log completed request with purpose context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eauditEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogAccessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eauditEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eDeterminePurposeFromRoute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePathString\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWithSegments\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/greeting\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;AccountManagement\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWithSegments\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/users/shipping-addresses\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;OrderFulfillment\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartsWithSegments\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/api/analytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;MarketingAnalytics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Unspecified\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eExtractTargetUserIdFromPath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePathString\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Extract user ID from path like /api/users/123/greeting\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epath\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSplit\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sc\"\u003e\u0026#39;/\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esegments\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;users\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003esegments\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration in Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePurposeAuditMiddleware\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"satisfying-article-15-access-requests\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#satisfying-article-15-access-requests\" title=\"Satisfying Article 15 Access Requests\"\u003eSatisfying Article 15 Access Requests\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEvery personal data access now includes the processing purpose in audit logs. When a GDPR Article 15 (Right of access) request arrives, you can generate a complete report showing every access to that user\u0026rsquo;s data, the stated purpose for each access, and whether those purposes align with original collection consent.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"monitoring-purpose-violations-with-health-checks\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#monitoring-purpose-violations-with-health-checks\" title=\"Monitoring Purpose Violations with Health Checks\"\u003eMonitoring Purpose Violations with Health Checks\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eASP.NET Core health checks can detect purpose drift by monitoring unauthorized access patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003ePurposeLimitationHealthCheck\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIHealthCheck\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIAuditLogRepository\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConsentRepository\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003ePurposeLimitationHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIAuditLogRepository\u003c/span\u003e \u003cspan class=\"n\"\u003eauditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIConsentRepository\u003c/span\u003e \u003cspan class=\"n\"\u003econsentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_auditRepository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eauditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_consentRepository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econsentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckHealthAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eHealthCheckContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elast24Hours\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(-\u003c/span\u003e\u003cspan class=\"m\"\u003e24\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Find accesses where purpose doesn\u0026#39;t match consent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_auditRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetAccessesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003esince\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003elast24Hours\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003efilter\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e200\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Successful accesses\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eaccess\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eviolations\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Unspecified\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e$\u0026#34;Unspecified purpose: {access.Endpoint} by {access.UserId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003econtinue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econsent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_consentRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetConsentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eaccess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTargetUserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eConsentPurposeFromString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccess\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePurpose\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econsent\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003econsent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsActive\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e$\u0026#34;No consent for {access.Purpose}: User {access.TargetUserId}, \u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"s\"\u003e$\u0026#34;Endpoint {access.Endpoint}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnhealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Detected {purposeViolations.Count} purpose limitation violations in last 24h\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;ViolationCount\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;Violations\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e$\u0026#34;Detected {purposeViolations.Count} potential violations\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e                    [\u0026#34;Violations\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epurposeViolations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthCheckResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;No purpose limitation violations detected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Registration in Program.cs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHealthChecks\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003ePurposeLimitationHealthCheck\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e\u0026#34;purpose-limitation\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003efailureStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eHealthStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDegraded\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003etags\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;privacy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;compliance\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"catching-drift-before-auditors-do\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#catching-drift-before-auditors-do\" title=\"Catching Drift Before Auditors Do\"\u003eCatching Drift Before Auditors Do\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis health check analyzes audit logs to identify data accesses without corresponding consent. If marketing analytics endpoints are being called for users who never consented to analytics, the health check fails, triggering alerts before a regulatory audit discovers the violation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-implementation-the-purpose-first-api-design-checklist\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#practical-implementation-the-purpose-first-api-design-checklist\" title=\"Practical Implementation: The Purpose-First API Design Checklist\"\u003ePractical Implementation: The Purpose-First API Design Checklist\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003ePurpose limitation transforms from abstract compliance requirement to concrete engineering practice through systematic API design:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBefore creating an endpoint:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDocument the specific processing purpose this endpoint serves\u003c/li\u003e\n\u003cli\u003eIdentify the minimum data fields required for that purpose\u003c/li\u003e\n\u003cli\u003eDetermine the legal basis (contract, legitimate interest, consent)\u003c/li\u003e\n\u003cli\u003eIf consent-based, verify consent validation logic exists\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eDuring implementation:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCreate purpose-specific DTOs that expose only required fields\u003c/li\u003e\n\u003cli\u003eImplement resource-based authorization validating purpose permission\u003c/li\u003e\n\u003cli\u003eAdd audit logging capturing the processing purpose\u003c/li\u003e\n\u003cli\u003eDocument the purpose and legal basis in API specifications\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003eAfter deployment:\u003c/strong\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eMonitor access logs for purpose/consent mismatches\u003c/li\u003e\n\u003cli\u003eReview authorization failures for legitimate use cases requiring consent\u003c/li\u003e\n\u003cli\u003eAudit endpoint usage patterns against original purpose documentation\u003c/li\u003e\n\u003cli\u003eUpdate consent capture flows if new purposes are introduced\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"beyond-compliance-purpose-as-architecture\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#beyond-compliance-purpose-as-architecture\" title=\"Beyond Compliance: Purpose as Architecture\"\u003eBeyond Compliance: Purpose as Architecture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eISO/IEC 27701 Control 7.2.6 (Limiting use, retention and disclosure) and GDPR Article 25 (Data protection by design) require purpose limitation not as a policy but as an architectural property. ASP.NET Core\u0026rsquo;s authorization framework, when applied to resources rather than just controllers, makes purpose-aware access control a natural extension of existing security patterns.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"identity-verification-is-not-purpose-validation\"\u003e\u003ca href=\"/posts/purpose-limitation-api-design/#identity-verification-is-not-purpose-validation\" title=\"Identity Verification Is Not Purpose Validation\"\u003eIdentity Verification Is Not Purpose Validation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe fatal mistake is treating all personal data as uniformly accessible once a user authenticates. The correct approach recognizes that data access requires both identity verification and purpose validation. A user proving who they are establishes the first authorization layer. The system determining why they need specific data establishes the second layer, the one that regulations actually mandate.\u003c/p\u003e\n\u003cp\u003ePurpose limitation is not an add-on feature. It\u0026rsquo;s the recognition that every API endpoint represents a contract: the caller states a purpose, and the API returns only data necessary to fulfill that purpose. When your endpoints leak more data than required, you\u0026rsquo;re not just violating regulatory standards. You\u0026rsquo;re violating the fundamental contract between data subjects and the systems that process their information.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-16T17:00:00+02:00","id":"https://daily-devops.net/posts/purpose-limitation-api-design/","language":"en","summary":"Why your API returns too much personal data and how ASP.NET Core resource-based authorization enforces data minimization at the endpoint level.","tags":["iso-standards","privacy","gdpr","dotnet","softwareengineering"],"title":"Purpose Limitation in API Design: Leaking Data You Shouldn't\n","url":"https://daily-devops.net/posts/purpose-limitation-api-design/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn \u003ca href=\"../code-sharpens-thinking/\"\u003ePart 1\u003c/a\u003e, we established that \u0026ldquo;vibe coding\u0026rdquo;—describing what you want and shipping what AI generates—creates productivity illusions that collapse spectacularly under production load. \u003ca href=\"../feedback-loop-ai-cant-replace/\"\u003ePart 2\u003c/a\u003e explored the feedback loop that AI can\u0026rsquo;t replicate.\u003c/p\u003e\n\u003cp\u003eNow we confront the practical question: \u003cstrong\u003eWhat skills define real professionals when typing code becomes trivial?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAI code assistants accelerate the mechanical part extraordinarily well. GitHub Copilot autocompletes functions. ChatGPT generates entire APIs from prompts. The typing is handled.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYet you remain indispensable.\u003c/strong\u003e Not in spite of AI\u0026rsquo;s code generation capabilities, but \u003cstrong\u003ebecause of them\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhy?\u003c/strong\u003e When code generation becomes commoditized, the differentiator isn\u0026rsquo;t typing speed. It\u0026rsquo;s accumulated experience. Watching systems fail in production. Understanding \u003cstrong\u003ewhy\u003c/strong\u003e they failed. Applying that hard-won knowledge to prevent the next failure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHere\u0026rsquo;s the uncomfortable truth:\u003c/strong\u003e Organizations that confuse \u0026ldquo;lines of code generated\u0026rdquo; with \u0026ldquo;productivity\u0026rdquo; discover the difference when production incidents spike—and the bill arrives.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-prompt-engineering-isnt-architecture\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#why-prompt-engineering-isnt-architecture\" title=\"Why Prompt Engineering Isn\u0026rsquo;t Architecture\"\u003eWhy Prompt Engineering Isn\u0026rsquo;t Architecture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eAI code generation creates a seductive trap.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYou think:\u003c/strong\u003e If I can describe what I want in natural language and get working code, isn\u0026rsquo;t that sufficient? Why spend time understanding implementation details when AI handles them?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHere\u0026rsquo;s why that\u0026rsquo;s wrong:\u003c/strong\u003e Prompts describe intent. Not constraints.\u003c/p\u003e\n\u003cp\u003eAnd software engineering? It\u0026rsquo;s fundamentally about managing \u003cstrong\u003econstraints\u003c/strong\u003e. Performance budgets. Memory limits. Concurrency safety. Error handling. Maintainability. Operational cost. Security boundaries.\u003c/p\u003e\n\u003cp\u003eConsider asking an AI to \u0026ldquo;implement caching for customer data.\u0026rdquo; You\u0026rsquo;ll get code that caches. But you \u003cstrong\u003ewon\u0026rsquo;t\u003c/strong\u003e get answers to:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhat\u0026rsquo;s the \u003cstrong\u003ememory budget\u003c/strong\u003e? When does caching become more expensive than repeated database calls?\u003c/li\u003e\n\u003cli\u003eHow do you handle \u003cstrong\u003ecache invalidation\u003c/strong\u003e across multiple application instances?\u003c/li\u003e\n\u003cli\u003eWhat\u0026rsquo;s the \u003cstrong\u003econsistency model\u003c/strong\u003e? Can stale data cause correctness issues downstream?\u003c/li\u003e\n\u003cli\u003eHow do you \u003cstrong\u003emonitor\u003c/strong\u003e cache hit rates to verify it\u0026rsquo;s actually improving performance?\u003c/li\u003e\n\u003cli\u003eWhat happens during \u003cstrong\u003ecache warming\u003c/strong\u003e? Do users experience degraded performance on cold starts?\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eAI generates code that addresses the prompt. Professionals understand these questions emerge from production experience\u003c/strong\u003e—from watching systems fail, from debugging race conditions at 3 AM, from analyzing cost reports that show caching is more expensive than the problem it solved, from responding to incidents where stale cache data caused customer-visible bugs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePrompt engineering optimizes for generating code quickly. Software architecture optimizes for systems that survive production reality. These are orthogonal skills.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve seen teams adopt AI-heavy workflows where \u003cstrong\u003ejunior developers generate features rapidly using prompts\u003c/strong\u003e, and \u003cstrong\u003esenior developers spend weeks later refactoring the accumulated technical debt\u003c/strong\u003e. The AI-generated code worked in isolation. It failed as a system because no one understood how the pieces interacted, what assumptions each component made, or where performance would degrade under load.\u003c/p\u003e\n\u003cp\u003eThe skill that AI can\u0026rsquo;t replace: \u003cstrong\u003erecognizing which questions to ask before writing code\u003c/strong\u003e, not generating syntax after questions are answered. That recognition comes from the feedback loop—you write code, watch it fail, understand \u003cstrong\u003ewhy\u003c/strong\u003e it failed, and internalize the lesson.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePrompt-driven development skips this loop entirely, outsourcing both the implementation and the learning.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eReal professionals don\u0026rsquo;t reject AI tools. They use them to accelerate the mechanical parts while maintaining ownership of the architectural decisions, performance analysis, and failure mode understanding that prompts can\u0026rsquo;t capture.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"technical-debt-where-abstract-design-becomes-concrete-burden\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#technical-debt-where-abstract-design-becomes-concrete-burden\" title=\"Technical Debt: Where Abstract Design Becomes Concrete Burden\"\u003eTechnical Debt: Where Abstract Design Becomes Concrete Burden\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTechnical debt is abstract thinking\u0026rsquo;s deferred consequences manifesting as maintenance burden. Design decisions that felt reasonable in isolation accumulate into complexity that resists change, harbors bugs, and drains productivity.\u003c/p\u003e\n\u003cp\u003eEvery architecture discussion includes statements like \u0026ldquo;we\u0026rsquo;ll refactor later\u0026rdquo; or \u0026ldquo;this is temporary\u0026rdquo; or \u0026ldquo;once we prove the concept, we\u0026rsquo;ll clean it up.\u0026rdquo; These are thought patterns that treat code as temporary scaffolding rather than operational reality. Code doesn\u0026rsquo;t stay temporary—it becomes production reality that teams maintain for years.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve inherited codebases where \u0026ldquo;temporary\u0026rdquo; solutions from 2015 still run in production, calcified by dependencies and surrounded by defensive code that works around their limitations. The abstract thinking that justified shortcuts—\u0026ldquo;we\u0026rsquo;re moving fast,\u0026rdquo; \u0026ldquo;we\u0026rsquo;ll fix it in v2\u0026rdquo;—never accounted for the operational reality: v2 got deprioritized, teams changed, knowledge evaporated, and the technical debt persisted.\u003c/p\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s own guidance on technical debt management emphasizes measurement and prioritization based on impact—not on abstract severity, but on actual operational burden:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Prioritize technical debt items based on their effects on workload functionality. Focus on addressing the issues that have the most significant effect on the performance, maintainability, and scalability of the workload.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis requires executable code that can be measured, profiled, and analyzed. Abstract architectural concerns translate into concrete technical debt only when code exists to evaluate. You can\u0026rsquo;t measure maintainability, performance impact, or operational cost without code that runs in production-like conditions.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAI-accelerated development amplifies this pattern.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eWhen junior developers generate features using prompts, the code works immediately but accumulates technical debt invisibly. The AI optimized for \u0026ldquo;works now,\u0026rdquo; not \u0026ldquo;maintainable long-term.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eSix months later, when requirements change? \u003cstrong\u003eThe bill comes due.\u003c/strong\u003e What took 2 days to generate takes 2 weeks to refactor. Why? Because no one understands the generated foundations.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReal cost:\u003c/strong\u003e Senior developers spending 40+ hours untangling AI-generated code instead of building new features. That\u0026rsquo;s €4,000-8,000 in lost productivity—per feature.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-professionals-in-the-ai-era-mastering-the-feedback-loop\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#real-professionals-in-the-ai-era-mastering-the-feedback-loop\" title=\"Real Professionals in the AI Era: Mastering the Feedback Loop\"\u003eReal Professionals in the AI Era: Mastering the Feedback Loop\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eDavid\u0026rsquo;s comment about real professionals not being replaced wasn\u0026rsquo;t wishful thinking or gatekeeping.\u003c/strong\u003e It was recognition that professional software engineering has \u003cstrong\u003enever\u003c/strong\u003e been about typing code—and in an era where typing is automated, that distinction becomes \u003cstrong\u003ebrutally\u003c/strong\u003e clear.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe skills that define professionals in 2026 and beyond:\u003c/strong\u003e\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"understanding-execution-characteristics\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#understanding-execution-characteristics\" title=\"Understanding Execution Characteristics\"\u003eUnderstanding Execution Characteristics\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen AI generates code, professionals can read it and immediately recognize:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAllocation patterns that will cause garbage collection pressure\u003c/li\u003e\n\u003cli\u003eDatabase access patterns that create N+1 problems\u003c/li\u003e\n\u003cli\u003eSynchronization primitives that risk deadlocks\u003c/li\u003e\n\u003cli\u003eAPI contracts that will break under versioning\u003c/li\u003e\n\u003cli\u003eAbstractions that trade clarity for cleverness\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis isn\u0026rsquo;t about memorizing syntax. It\u0026rsquo;s about pattern recognition from seeing thousands of implementations and their production consequences. AI can generate the code. Professionals can predict where it fails before deployment.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"asking-questions-ai-cant-formulate\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#asking-questions-ai-cant-formulate\" title=\"Asking Questions AI Can\u0026rsquo;t Formulate\"\u003eAsking Questions AI Can\u0026rsquo;t Formulate\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI optimizes for the prompt it receives. Professionals know which questions to ask before prompting:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhat\u0026rsquo;s the failure mode if this service is unavailable?\u003c/li\u003e\n\u003cli\u003eHow does this perform when the dataset grows 100x?\u003c/li\u003e\n\u003cli\u003eWhat happens during partial failures across service boundaries?\u003c/li\u003e\n\u003cli\u003eHow do we roll this back if production deployment reveals problems?\u003c/li\u003e\n\u003cli\u003eWhat operational metrics signal that this implementation is degrading?\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese questions emerge from production scars, not documentation. They represent thinking that can\u0026rsquo;t be prompted because the prompt itself requires experience to formulate.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"recognizing-when-ai-solutions-are-wrong\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#recognizing-when-ai-solutions-are-wrong\" title=\"Recognizing When AI Solutions Are Wrong\"\u003eRecognizing When AI Solutions Are Wrong\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI generates plausible code. Professionals recognize when plausible diverges from correct:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe generated caching looks reasonable but introduces race conditions\u003c/li\u003e\n\u003cli\u003eThe suggested refactoring breaks semantic guarantees the original code maintained\u003c/li\u003e\n\u003cli\u003eThe performance optimization trades correctness for speed\u003c/li\u003e\n\u003cli\u003eThe error handling silences failures that should propagate\u003c/li\u003e\n\u003cli\u003eThe abstraction solves the described problem but makes the actual problem harder\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis skill—recognizing subtle wrongness—requires understanding not just what code does, but what it should do in context. AI has no context beyond the prompt. Professionals carry context from the entire system, the organization\u0026rsquo;s constraints, and production failure history.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"debugging-when-ai-generated-code-fails\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#debugging-when-ai-generated-code-fails\" title=\"Debugging When AI-Generated Code Fails\"\u003eDebugging When AI-Generated Code Fails\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI can\u0026rsquo;t debug its own output effectively because it has no execution model. It can suggest changes based on error messages, but it can\u0026rsquo;t reason about:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhy the garbage collector is thrashing\u003c/li\u003e\n\u003cli\u003eWhere the memory leak originates across object graphs\u003c/li\u003e\n\u003cli\u003eWhy this specific race condition appears under production load but not in testing\u003c/li\u003e\n\u003cli\u003eHow this performance degradation emerged from the interaction of six separate components\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eProfessionals debug by understanding execution: what the CPU is doing, how memory is managed, where I/O blocking occurs, how the runtime schedules work. This understanding comes from the feedback loop—watching code execute, measuring behavior, correlating symptoms with causes.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"maintaining-code-ai-generated-yesterday\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#maintaining-code-ai-generated-yesterday\" title=\"Maintaining Code AI Generated Yesterday\"\u003eMaintaining Code AI Generated Yesterday\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe code AI generates today becomes the maintenance burden of tomorrow. Professionals understand that maintainability isn\u0026rsquo;t syntax elegance—it\u0026rsquo;s whether future developers (including AI-assisted ones) can understand intent, modify behavior safely, and reason about consequences.\u003c/p\u003e\n\u003cp\u003eAI-generated code often optimizes for immediate functionality over long-term maintainability because prompts rarely include \u0026ldquo;make this easy to modify in six months when requirements change.\u0026rdquo; Professionals review AI output through the lens of future maintenance: Does this abstraction clarify or obscure? Will this pattern scale when similar features are added? Can someone unfamiliar with this code understand its failure modes?\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-economic-reality-of-ai-accelerated-development\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#the-economic-reality-of-ai-accelerated-development\" title=\"The Economic Reality of AI-Accelerated Development\"\u003eThe Economic Reality of AI-Accelerated Development\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI tools make junior developers dramatically more productive at generating code. Sounds like pure upside, right?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUntil you measure the total lifecycle cost:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eFeatures ship faster \u003cstrong\u003ebut\u003c/strong\u003e accumulate technical debt faster\u003c/li\u003e\n\u003cli\u003eCode coverage is high \u003cstrong\u003ebut\u003c/strong\u003e defect rates increase by 25-40%\u003c/li\u003e\n\u003cli\u003eDevelopment velocity looks impressive \u003cstrong\u003euntil\u003c/strong\u003e production incidents spike\u003c/li\u003e\n\u003cli\u003eRefactoring becomes more expensive because no one understands the AI-generated foundations\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eTwo types of organizations:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eType 1 measures productivity by lines of code generated or features shipped per sprint. They see AI as a massive win.\u003c/p\u003e\n\u003cp\u003eType 2 measures productivity by system reliability, operational cost, and maintenance burden. They see a more complex picture—and higher total cost.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eYour value proposition shifts:\u003c/strong\u003e From \u0026ldquo;can write code\u0026rdquo; to \u0026ldquo;can ensure AI-generated code survives production.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not a diminished role. \u003cstrong\u003eIt\u0026rsquo;s a more critical one.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThe ability to generate code becomes commoditized. The ability to evaluate, refine, and maintain that code? \u003cstrong\u003eThat becomes your differentiator.\u003c/strong\u003e\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-the-feedback-loop-cant-be-automated\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#why-the-feedback-loop-cant-be-automated\" title=\"Why the Feedback Loop Can\u0026rsquo;t Be Automated\"\u003eWhy the Feedback Loop Can\u0026rsquo;t Be Automated\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI can participate in parts of the feedback loop:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIt can suggest implementations based on requirements\u003c/li\u003e\n\u003cli\u003eIt can generate tests based on code\u003c/li\u003e\n\u003cli\u003eIt can propose refactorings based on patterns\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBut it can\u0026rsquo;t close the loop because closing the loop requires:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eExecution in realistic conditions\u003c/strong\u003e: Production load, real data volumes, actual failure scenarios\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeasurement of consequences\u003c/strong\u003e: Performance under stress, cost implications, operational burden\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInterpretation of results\u003c/strong\u003e: Understanding why this metric degraded, why this pattern emerged, why this assumption failed\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRefinement of thinking\u003c/strong\u003e: Updating mental models about what works, what fails, and why\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eApplication to future decisions\u003c/strong\u003e: Recognizing similar patterns in new contexts and avoiding repeated mistakes\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAI can help with steps 1 and 2. Steps 3, 4, and 5 require human judgment informed by accumulated experience. This is the feedback loop David referenced—the mechanism that sharpens thinking through repeated collision with executable reality.\u003c/p\u003e\n\u003cp\u003eReal professionals master this loop. They write code (or review AI-generated code), watch it execute, measure its behavior, understand its failure modes, and refine their thinking. Each iteration strengthens their ability to recognize what will work before writing it, what will fail before deploying it, and what will cost more than it\u0026rsquo;s worth before building it.\u003c/p\u003e\n\u003cp\u003eThis skill can\u0026rsquo;t be replaced because it\u0026rsquo;s not about having the right answer immediately—it\u0026rsquo;s about knowing how to find the right answer through disciplined iteration between abstract thinking and concrete execution.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion-code-demands-honest-thinking\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#conclusion-code-demands-honest-thinking\" title=\"Conclusion: Code Demands Honest Thinking\"\u003eConclusion: Code Demands Honest Thinking\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYes, thinking is hard. Reasoning through constraints, evaluating trade-offs, understanding system dynamics—these require deep intellectual work. \u003cstrong\u003eI\u0026rsquo;ve never disputed this.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eBut \u003cstrong\u003ehere\u0026rsquo;s what the \u0026ldquo;thinking is everything\u0026rdquo; narrative misses:\u003c/strong\u003e code is not just the mechanical output of that thinking. Code is the form that \u003cstrong\u003eforces thinking into honesty\u003c/strong\u003e. It\u0026rsquo;s where vague reasoning gets \u003cstrong\u003ebrutally exposed\u003c/strong\u003e, deferred decisions become \u003cstrong\u003eunavoidable\u003c/strong\u003e, and abstract consequences materialize as \u003cstrong\u003eoperational reality that costs real money and wakes you up at 3 AM\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eTreating code as \u0026ldquo;just another language\u0026rdquo; undersells what programming actually does: it transforms thought from abstract possibility into \u003cstrong\u003eexecutable certainty\u003c/strong\u003e. It makes performance \u003cstrong\u003emeasurable\u003c/strong\u003e, correctness \u003cstrong\u003etestable\u003c/strong\u003e, and complexity \u003cstrong\u003evisible\u003c/strong\u003e. It forces precision where thought allows \u003cstrong\u003ecomfortable ambiguity\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSoftware engineering isn\u0026rsquo;t thinking OR programming. It\u0026rsquo;s thinking made rigorous through programming.\u003c/strong\u003e It\u0026rsquo;s the tight feedback loop where abstract reasoning and executable verification sharpen each other iteratively.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOne without the other doesn\u0026rsquo;t scale:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThinking without executable form stays untested and \u003cstrong\u003eoften wrong\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eCode without thoughtful design becomes \u003cstrong\u003eunmaintainable complexity\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eAI-generated code without understanding becomes \u003cstrong\u003etechnical debt that compounds with every sprint\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003ePrompt engineering without production experience becomes \u003cstrong\u003ea liability dressed as productivity\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEngineering quality emerges from the discipline of moving between abstract reasoning and concrete implementation—\u003cstrong\u003erepeatedly, rigorously, honestly\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThat\u0026rsquo;s what makes software engineering difficult.\u003c/strong\u003e Not the typing. Not even just the thinking. But the intellectual discipline of forcing thought into executable form that \u003cstrong\u003esurvives contact with production reality\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eAI can type code faster than you. It can suggest implementations, generate tests, propose refactorings. \u003cstrong\u003eWhat it can\u0026rsquo;t do is learn from watching systems fail in production, understand why they failed, and apply that hard-won knowledge to prevent the next failure.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAnd that discipline, the feedback loop David referenced, cannot be replaced.\u003c/strong\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"series-summary\"\u003e\u003ca href=\"/posts/real-professional-software-engineering-ai-era/#series-summary\" title=\"Series Summary\"\u003eSeries Summary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePart 1: \u003ca href=\"../code-sharpens-thinking/\"\u003eWhy Real Professionals Will Never Be Replaced by AI\u003c/a\u003e\u003c/strong\u003e\u003cbr\u003e\nEstablished that AI-generated code without understanding creates productivity illusions. Vibe coding collapses when code generation becomes trivial and understanding execution, failure modes, and operational cost becomes everything.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 2: \u003ca href=\"../feedback-loop-ai-cant-replace/\"\u003eThe Feedback Loop That AI Can\u0026rsquo;t Replace\u003c/a\u003e\u003c/strong\u003e\u003cbr\u003e\nExamined the mechanisms that transform abstract thinking into operational understanding: compilers validate logic, tests expose behavioral gaps, profilers measure performance reality, production reveals deferred decisions.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 3: Real Professional Software Engineering in the AI Era\u003c/strong\u003e (this article)\u003cbr\u003e\nExplored the irreplaceable professional skillset: recognizing execution characteristics, asking questions AI can\u0026rsquo;t formulate, debugging failures AI can\u0026rsquo;t reason about, maintaining code AI generated yesterday, and understanding the economic reality where \u0026ldquo;AI productivity\u0026rdquo; often means faster technical debt accumulation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe throughline:\u003c/strong\u003e Real professionals will never be replaced because they\u0026rsquo;ve mastered the feedback loop: the iterative discipline of writing code, watching it fail, understanding why, and refining thinking. AI participates in parts of this loop but can\u0026rsquo;t close it. That\u0026rsquo;s where professionals remain indispensable.\u003c/p\u003e\n","date_modified":"2026-05-25T22:06:34+02:00","date_published":"2026-01-20T17:00:00+01:00","id":"https://daily-devops.net/posts/real-professional-software-engineering-ai-era/","language":"en","summary":"AI generates code instantly. Professionals spot when it is subtly wrong, debug failures AI cannot reason about, and see through the productivity narrative.\n","tags":["softwareengineering","codequality","bestpractices","architecture","dotnet","csharp","technicaldebt","ai-code-assistant","github-copilot"],"title":"Real Professional Software Engineering in the AI Era\n","url":"https://daily-devops.net/posts/real-professional-software-engineering-ai-era/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn \u003ca href=\"../code-sharpens-thinking/\"\u003ePart 1 of this series\u003c/a\u003e, we explored why AI code generation creates an illusion of productivity that collapses when \u0026ldquo;vibe coding\u0026rdquo; meets production reality.\u003c/p\u003e\n\u003cp\u003eTyping code is now trivial. AI handles it faster than humans can type.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBut here\u0026rsquo;s the critical skill:\u003c/strong\u003e Understanding what that code costs. Where it fails. Why it breaks under load.\u003c/p\u003e\n\u003cp\u003eThe differentiator between professionals and prompt engineers? \u003cstrong\u003eThe feedback loop.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eYou write code (or review AI-generated code). Watch it execute. Measure its behavior. Understand its failure modes. Refine your thinking. Each iteration sharpens your ability to recognize what will work before implementing it, what will fail before deploying it, and what will cost more than it\u0026rsquo;s worth before building it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSo what exactly is this feedback loop? And why can\u0026rsquo;t AI replicate it?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThis article examines the mechanisms that transform abstract thinking into operational understanding:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCompilers\u003c/strong\u003e that validate logical consistency and force completeness\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePerformance profilers\u003c/strong\u003e that expose what abstract analysis defers\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTesting frameworks\u003c/strong\u003e that reveal behavioral gaps\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProduction environments\u003c/strong\u003e that materialize every deferred decision\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese aren\u0026rsquo;t just development tools—they\u0026rsquo;re thinking validators that expose where reasoning was incomplete. AI can participate in parts of this loop, but it can\u0026rsquo;t close it. Understanding why reveals why real professionals remain irreplaceable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-compiler-as-thought-validator\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#the-compiler-as-thought-validator\" title=\"The Compiler as Thought Validator\"\u003eThe Compiler as Thought Validator\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModern compilers do more than translate syntax—they validate logical consistency. Static analysis, type checking, nullability analysis, and pattern exhaustiveness checks all function as automated reasoning validators. They catch the gaps that pure thought leaves unresolved.\u003c/p\u003e\n\u003cp\u003eConsider exhaustive pattern matching introduced in C# 8:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eenum\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ePending\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eConfirmed\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eShipped\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDelivered\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancelled\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetStatusMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePending\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order is pending\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfirmed\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order confirmed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eShipped\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order shipped\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Compiler error CS8509: The switch expression does not handle all possible values\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe compiler refuses to accept incomplete reasoning. In abstract discussion, you might focus on the \u0026ldquo;normal\u0026rdquo; states and unconsciously ignore edge cases. The compiler forces completeness.\u003c/p\u003e\n\u003cp\u003eOr consider cyclomatic complexity analysis built into Visual Studio and available through analyzers. High complexity scores (typically above 10) indicate control flow that\u0026rsquo;s difficult to reason about and test thoroughly. The code analyzer doesn\u0026rsquo;t just flag style violations—it measures cognitive load and highlights where thinking has likely become too tangled to maintain reliably.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Complexity: 15 (Warning CS1591)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateDiscount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eorderDate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsPremium\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorderDate\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMonth\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.25\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eYearsActive\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.20\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.15\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e500\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.10\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.05\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003eorderDate\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMonth\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.15\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e500\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0.05\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis method might make sense in abstract discussion: \u0026ldquo;We give discounts based on customer status, order size, and date.\u0026rdquo; But complexity analysis reveals what abstract thinking hides—the decision tree is convoluted, error-prone, and unmaintainable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLook closer at the logic:\u003c/strong\u003e Business rules state that long-term premium customers (6+ years) should get the highest discount (30%) for high-value orders—even better than the December holiday bonus. But a premium customer with 7 years active ordering €1,500 in December only gets 25%—the \u003ccode\u003eelse if (customer.YearsActive \u0026gt; 5)\u003c/code\u003e branch returning 0.20m is \u003cstrong\u003eunreachable\u003c/strong\u003e because the December check already returned. \u003cstrong\u003eThe nested if-structure makes the bug invisible in code review but obvious when a test fails:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Fact]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateDiscount_LoyalPremiumCustomer_December_ShouldGetLoyaltyBonus\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Long-term customers should get loyalty discount even in December\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsPremium\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eYearsActive\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e7\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1500\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2025\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ediscount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_calculator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateDiscount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0.30\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ediscount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// FAILS: Returns 0.25m instead\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                                    \u003cspan class=\"c1\"\u003e// The YearsActive\u0026gt;5 branch is unreachable!\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe code forces you to confront what clean thinking would have structured differently:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Complexity: 4\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateDiscount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e \u003cspan class=\"n\"\u003eorderDate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003erules\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDiscountRuleEngine\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePremiumCustomerRule\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHighValueOrderRule\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddRule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHolidayPromotionRule\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003erules\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateDiscount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorderDate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRefactoring didn\u0026rsquo;t just clean up syntax—it exposed and resolved structural thinking problems that abstract reasoning missed. The rule engine evaluates all rules and picks the highest discount, making the business logic explicit and the bug impossible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-where-theory-meets-production-reality\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#performance-where-theory-meets-production-reality\" title=\"Performance: Where Theory Meets Production Reality\"\u003ePerformance: Where Theory Meets Production Reality\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAlgorithmic complexity feels manageable in theoretical discussion. O(n) sounds reasonable. O(n²) seems acceptable for small datasets. O(n log n) feels efficient. Then production traffic hits, datasets grow larger than anticipated, and theoretical complexity translates into CPU cost, memory pressure, and timeout failures.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve debugged production incidents where perfectly logical code—code that passed all functional tests—caused cascading performance failures. \u003cstrong\u003eHours wasted.\u003c/strong\u003e Customer complaints. Emergency hotfixes.\u003c/p\u003e\n\u003cp\u003eWhy? Complexity analysis happened in abstract terms rather than executable measurement.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eExample:\u003c/strong\u003e Nested LINQ queries that looked clean and expressive during development:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Looks elegant, reads well\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderSummary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomerOrders\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_orders\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderSummary\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eli\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrice\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQuantity\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eItemCount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eCategories\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eli\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCategory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDistinct\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderBy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToList\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToList\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis code communicates intent clearly. In abstract reasoning, it feels straightforward: \u0026ldquo;Get orders, calculate summaries, sort by total.\u0026rdquo; But execute it with real data and watch database query patterns, memory allocations, and execution time:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eMultiple database round trips per order (N+1 query problem)\u003c/li\u003e\n\u003cli\u003eRepeated calculations over the same collections\u003c/li\u003e\n\u003cli\u003eUnnecessary allocations for intermediate collections\u003c/li\u003e\n\u003cli\u003eLinear scans for categories on every line item\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe abstract reasoning missed what executable profiling makes obvious:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Same intent, different execution characteristics\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderSummary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomerOrders\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_context\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrders\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThenInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eli\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThenInclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCategory\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsNoTracking\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToListAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderSummary\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eli\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrice\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQuantity\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eItemCount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eCategories\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLineItems\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSelect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eli\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eli\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProduct\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCategory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDistinct\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToList\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToList\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCode forced the performance implications into measurable form. Profiling revealed what abstract thought deferred—database round trips, allocation patterns, execution cost. Without writing and measuring executable code, these consequences remain invisible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-feedback-loop-programming-provides\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#the-feedback-loop-programming-provides\" title=\"The Feedback Loop Programming Provides\"\u003eThe Feedback Loop Programming Provides\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eProgramming isn\u0026rsquo;t just thinking\u0026rsquo;s output—it\u0026rsquo;s thinking\u0026rsquo;s verification mechanism. The discipline of translating thought into executable form exposes inconsistencies, reveals missing decisions, and surfaces consequences that abstract reasoning defers.\u003c/p\u003e\n\u003cp\u003eThis feedback loop operates at multiple levels:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"compilation-immediate-logical-feedback\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#compilation-immediate-logical-feedback\" title=\"Compilation: Immediate Logical Feedback\"\u003eCompilation: Immediate Logical Feedback\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe compiler catches type mismatches, null reference possibilities, exhaustiveness gaps, and logical inconsistencies within seconds. No mental review provides this consistency and speed.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"testing-behavioral-verification\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#testing-behavioral-verification\" title=\"Testing: Behavioral Verification\"\u003eTesting: Behavioral Verification\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnit tests, integration tests, and property-based tests validate that your mental model of system behavior matches actual execution. I\u0026rsquo;ve written tests expecting specific behavior only to discover the code does something entirely different—not because implementation was wrong, but because reasoning was incomplete.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Fact]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateDiscount_PremiumCustomer_HighValue_December_Returns25Percent\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Test reveals the logic we thought we implemented doesn\u0026#39;t match what we actually coded\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsPremium\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eYearsActive\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1500\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2025\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ediscount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_calculator\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateDiscount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0.25\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ediscount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Fails: Returns 0.15m instead\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe test didn\u0026rsquo;t catch a bug in isolation—it caught incomplete thinking that manifested as unexpected behavior. Without executable code and explicit testing, that gap stays hidden until production.\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"the-ai-testing-trap\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#the-ai-testing-trap\" title=\"The AI Testing Trap\"\u003eThe AI Testing Trap\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003eAI can generate tests as easily as it generates implementations. Ask for unit tests, and you\u0026rsquo;ll get methods that exercise code paths and verify outputs. \u003cstrong\u003eThis creates a dangerous illusion: high code coverage with low confidence.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAI-generated tests typically verify \u003cstrong\u003ehappy paths\u003c/strong\u003e—the scenarios explicitly described in prompts. They \u003cstrong\u003erarely\u003c/strong\u003e test:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eEdge cases that emerge from domain understanding\u003c/li\u003e\n\u003cli\u003eConcurrency issues that only appear under load\u003c/li\u003e\n\u003cli\u003eError propagation through system boundaries\u003c/li\u003e\n\u003cli\u003eIntegration failures when dependencies behave unexpectedly\u003c/li\u003e\n\u003cli\u003ePerformance degradation with realistic data volumes\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eI\u0026rsquo;ve reviewed codebases with 90%+ test coverage where AI generated both implementation and tests.\u003c/strong\u003e Every test passed. Yet production revealed critical bugs because the tests verified that the code did \u003cstrong\u003ewhat it was written to do\u003c/strong\u003e, not that it \u003cstrong\u003esolved the actual problem correctly\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe professional\u0026rsquo;s advantage:\u003c/strong\u003e knowing what to test comes from understanding how systems fail in production. That knowledge \u003cstrong\u003ecan\u0026rsquo;t be prompted\u003c/strong\u003e—it must be experienced, internalized, and applied deliberately.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"profiling-performance-reality-check\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#profiling-performance-reality-check\" title=\"Profiling: Performance Reality Check\"\u003eProfiling: Performance Reality Check\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eProfilers measure actual CPU consumption, memory allocation patterns, I/O bottlenecks, and threading contention. Abstract complexity analysis (Big-O notation) provides theoretical bounds. Profiling provides operational reality.\u003c/p\u003e\n\u003cp\u003eVisual Studio\u0026rsquo;s .NET Object Allocation tool shows exactly which code paths allocate memory and how much. BenchmarkDotNet provides precise execution timing with statistical analysis. These tools don\u0026rsquo;t just measure code—they validate or invalidate reasoning about performance characteristics.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[MemoryDiagnoser]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eStringBuildingBenchmark\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Benchmark]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eConcatenationInLoop\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Abstract: \u0026#34;Should be fine for 1000 iterations\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Benchmark]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilderInLoop\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Results expose reality abstract thinking missed:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// ConcatenationInLoop:  3,450 μs,  allocated: 2,031,616 B\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// StringBuilderInLoop:     45 μs,  allocated:     24,624 B\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe difference between \u0026ldquo;seems reasonable\u0026rdquo; and \u0026ldquo;actually performs\u0026rdquo; is 75x execution time and 80x memory allocation. Abstract reasoning deferred these consequences. Executable code and measurement made them visible.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"production-ultimate-reality-validation\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#production-ultimate-reality-validation\" title=\"Production: Ultimate Reality Validation\"\u003eProduction: Ultimate Reality Validation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eProduction exposes every assumption abstract thinking made: scale, concurrency, failure modes, dependency availability, network latency, operational complexity. Code that worked flawlessly in development reveals hidden assumptions when deployed at scale with real users, real data, and real failure conditions.\u003c/p\u003e\n\u003cp\u003eMonitoring, telemetry, and distributed tracing provide feedback about system behavior under actual conditions. Without executable code running in production, all architectural reasoning remains theoretical.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"programming-and-thinking-inseparable-not-sequential\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#programming-and-thinking-inseparable-not-sequential\" title=\"Programming and Thinking: Inseparable, Not Sequential\"\u003eProgramming and Thinking: Inseparable, Not Sequential\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe original framing positioned thinking and programming sequentially: think first (the hard part), then program (the easy translation). This model fundamentally misrepresents the relationship.\u003c/p\u003e\n\u003cp\u003eProgramming and thinking are inseparable, iterative, and mutually reinforcing:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAbstract thinking\u003c/strong\u003e identifies problems, explores solution spaces, and proposes approaches.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCode writing\u003c/strong\u003e forces abstraction into precise, executable form, exposing gaps and inconsistencies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExecution and measurement\u003c/strong\u003e reveal consequences—performance, resource consumption, failure modes—that abstract thought deferred.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRefinement\u003c/strong\u003e incorporates execution reality back into thinking, improving the mental model.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRepeat\u003c/strong\u003e until thinking and execution align.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eNeither operates effectively alone. Thinking without code stays vague and unvalidated. Code without thinking becomes mechanical translation without understanding. High-quality software emerges from tight iteration between abstract reasoning and executable verification.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t pedantry about implementation details. This is recognition that software engineering is fundamentally about managing complexity in executable systems. Complexity that can\u0026rsquo;t be reasoned about produces brittle, unmaintainable systems. Complexity that remains purely abstract never confronts operational reality.\u003c/p\u003e\n\u003cp\u003eThe discipline of programming (writing code, measuring behavior, refactoring based on feedback) is how abstract thinking becomes operational reality. It\u0026rsquo;s not the easy part that follows hard thinking. It\u0026rsquo;s the verification mechanism that sharpens thinking and exposes where reasoning was incomplete.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-makes-professionals-irreplaceable\"\u003e\u003ca href=\"/posts/feedback-loop-ai-cant-replace/#what-makes-professionals-irreplaceable\" title=\"What Makes Professionals Irreplaceable\"\u003eWhat Makes Professionals Irreplaceable\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompilers validate logic. Tests reveal behavioral gaps. Profilers measure performance reality. Production exposes every deferred decision. These tools generate feedback constantly.\u003c/p\u003e\n\u003cp\u003eBut feedback is worthless without interpretation. And interpretation requires experience.\u003c/p\u003e\n\u003cp\u003eWhen a profiler shows 75x performance degradation, the junior developer sees a red flag. The senior engineer sees a memory allocation pattern they\u0026rsquo;ve debugged before, recognizes the architectural constraint it reveals, and knows three ways to fix it based on context. When production monitoring shows intermittent timeout spikes, AI suggests retry logic. The experienced architect recognizes a connection pool exhaustion pattern and addresses the root cause.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe irreplaceable skill isn\u0026rsquo;t generating code. It\u0026rsquo;s closing the feedback loop.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThat means watching code fail, understanding \u003cem\u003ewhy\u003c/em\u003e it fails, and refining your mental model until your intuition predicts failure modes before they manifest. AI participates in generating code and even in analyzing errors. But it can\u0026rsquo;t internalize the lessons. It can\u0026rsquo;t build the judgment that comes from years of production incidents, debugging sessions, and architectural decisions that played out over time.\u003c/p\u003e\n\u003cp\u003eIn the \u003ca href=\"../real-professional-software-engineering-ai-era/\"\u003efinal part of this series\u003c/a\u003e, we\u0026rsquo;ll examine what this means for professional development. When code generation is commoditized, what skills actually matter? How do you build the cognitive architecture that AI can\u0026rsquo;t replicate?\u003c/p\u003e\n\u003cp\u003eThe answer shapes how we train developers, evaluate expertise, and define what \u0026ldquo;senior engineer\u0026rdquo; means in an AI-augmented world.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-15T17:00:00+01:00","id":"https://daily-devops.net/posts/feedback-loop-ai-cant-replace/","language":"en","summary":"Compilers validate logic, profilers expose performance lies, and production reveals every deferred decision. AI cannot close that feedback loop for you.\n","tags":["softwareengineering","codequality","bestpractices","architecture","dotnet","csharp","technicaldebt","ai-code-assistant","github-copilot"],"title":"The Feedback Loop That AI Can't Replace\n","url":"https://daily-devops.net/posts/feedback-loop-ai-cant-replace/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn Swabia, a region in southern Germany, there\u0026rsquo;s a cultural concept that outsiders tend to misunderstand as a local joke. \u003cem\u003eKehrwoche\u003c/em\u003e is often translated as \u0026ldquo;sweeping week,\u0026rdquo; which sounds harmless, almost folkloric. In reality, it\u0026rsquo;s a social mechanism for enforcing long-term responsibility in shared environments.\u003c/p\u003e\n\u003cp\u003eEvery household gets assigned a recurring time slot in which it must clean communal areas—stairwells, hallways, sidewalks in front of the building. The scope is clearly defined. The cadence is predictable. The expectation is absolute. What makes this system effective isn\u0026rsquo;t enforcement by authority, but enforcement by culture. Skipping your turn isn\u0026rsquo;t illegal, but it\u0026rsquo;s socially expensive.\u003c/p\u003e\n\u003cp\u003eYour neighbors know when it\u0026rsquo;s your week. They notice if the stairs look questionable on Tuesday morning. They\u0026rsquo;ll mention it. Not aggressively—just a casual observation that somehow carries the weight of communal judgment. The enforcement mechanism isn\u0026rsquo;t bureaucratic. It\u0026rsquo;s the quiet awareness that someone is watching, and someone remembers.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t surveillance in the oppressive sense. It\u0026rsquo;s accountability baked into the social contract. You\u0026rsquo;re not cleaning because Big Brother demands it. You\u0026rsquo;re cleaning because Mrs. Schmid from the second floor has standards, and you\u0026rsquo;re going to face her in the elevator tomorrow.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eKehrwoche\u003c/em\u003e is scarier than breaking the build on Friday afternoon. At least the build doesn\u0026rsquo;t remember next Tuesday, and it won\u0026rsquo;t give you that look in the hallway.\u003c/p\u003e\n\u003cp\u003eThat dynamic should feel uncomfortably familiar to anyone who has worked on a software system longer than a few months.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-kehrwoche-really-means\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#what-kehrwoche-really-means\" title=\"What Kehrwoche Really Means\"\u003eWhat \u003cem\u003eKehrwoche\u003c/em\u003e Really Means\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003eKehrwoche\u003c/em\u003e isn\u0026rsquo;t about cleanliness as an outcome. It\u0026rsquo;s about preventing entropy from becoming visible.\u003c/p\u003e\n\u003cp\u003eNo one expects perfection. Dust will return. Dirt is inevitable. The goal isn\u0026rsquo;t to eliminate mess, but to ensure it never reaches a point where it disrupts daily life. Maintenance is continuous, not reactive.\u003c/p\u003e\n\u003cp\u003eThis mindset is fundamentally different from how many software teams approach quality. In practice, teams often accept slow degradation until it becomes painful enough to justify a large intervention. \u003cem\u003eKehrwoche\u003c/em\u003e deliberately avoids that escalation by making small maintenance unavoidable and routine.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s discipline by design, not by motivation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-direct-translation-to-software-development\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#the-direct-translation-to-software-development\" title=\"The Direct Translation to Software Development\"\u003eThe Direct Translation to Software Development\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTechnical debt behaves exactly like dirt in shared spaces. Some of it is created intentionally. Some of it accumulates unintentionally. None of it disappears on its own.\u003c/p\u003e\n\u003cp\u003eThe critical mistake many teams make is treating technical debt as an abstract future concern. It gets tracked in tickets, discussed in retrospectives, and postponed in planning meetings. Over time, it becomes normalized background noise.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eKehrwoche\u003c/em\u003e doesn\u0026rsquo;t allow for that kind of abstraction. Responsibility is assigned to people, not to the building. In software, responsibility often dissolves into process, tooling, or vague ownership models. When everyone owns the code, nobody cleans it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-everyone-owns-the-code\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#when-everyone-owns-the-code\" title=\"When Everyone Owns The Code\"\u003eWhen Everyone Owns The Code\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eI\u0026rsquo;ve seen this pattern repeat itself across enterprise teams. Build times creep from five minutes to twenty. Test suites grow flaky. Configuration files accumulate \u0026ldquo;temporary\u0026rdquo; exceptions that live for years. Each change makes sense locally. The aggregate effect is paralysis.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-stairwell-problem-in-codebases\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#the-stairwell-problem-in-codebases\" title=\"The Stairwell Problem in Codebases\"\u003eThe Stairwell Problem in Codebases\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eShared infrastructure is where technical debt becomes most toxic.\u003c/p\u003e\n\u003cp\u003eBuild pipelines grow slower and more fragile. Configuration files accumulate exceptions. Authentication flows gain special cases that no one fully understands anymore. These aren\u0026rsquo;t edge areas of the system. They\u0026rsquo;re the paths every developer walks through daily.\u003c/p\u003e\n\u003cp\u003eLike a stairwell, these areas are rarely \u0026ldquo;owned\u0026rdquo; by a single team. They evolve through small, justified changes that make sense locally and degrade the global structure over time. No single change is catastrophic. The aggregate effect is.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-a-pipeline-becomes-a-stairwell\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#how-a-pipeline-becomes-a-stairwell\" title=\"How A Pipeline Becomes A Stairwell\"\u003eHow A Pipeline Becomes A Stairwell\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsider a CI/CD pipeline. Year one, it\u0026rsquo;s ten lines of configuration: trigger on main branch, run the build. Clean. Obvious. Everyone understands it.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003etrigger\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebranches\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003emain]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003esteps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003etask\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDotNetCoreCLI@2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThree years later, it\u0026rsquo;s dirty, slow, and fragile. Every team has added exceptions to accommodate their needs. Flaky tests are skipped. Build steps are conditionally executed based on branch names. Maintenance tasks are shoehorned into the pipeline because \u0026ldquo;there\u0026rsquo;s no better place.\u0026rdquo; The configuration file has ballooned to hundreds of lines.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003etrigger\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ebranches\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003emain, release/*, hotfix/*]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003epaths\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eexclude\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"l\"\u003edocs/*, \u0026#39;*.md\u0026#39;, \u0026#39;!critical.md\u0026#39;]\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003esteps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003etask\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDotNetCoreCLI@2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003econdition\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eand(succeeded(), ne(variables[\u0026#39;Skip.Build\u0026#39;], \u0026#39;true\u0026#39;))\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c\"\u003e# ... plus 15 more lines of arguments and workarounds\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEach addition solved a real problem. \u0026ldquo;We need to skip flaky tests until they\u0026rsquo;re fixed.\u0026rdquo; \u0026ldquo;Let\u0026rsquo;s exclude docs to speed up builds.\u0026rdquo; \u0026ldquo;Add a variable to disable builds when we\u0026rsquo;re doing maintenance.\u0026rdquo; Every decision was rational. Every comment explained the context. Nobody was careless.\u003c/p\u003e\n\u003cp\u003eBut nobody was responsible for the aggregate state. The pipeline became the project\u0026rsquo;s stairwell—walked through daily, maintained by nobody, degrading incrementally until simple changes became risky.\u003c/p\u003e\n\u003cp\u003eAt that point, teams don\u0026rsquo;t complain about dirt. They complain about friction, unpredictability, and fear of change. The dirt metaphor just becomes technical language.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-big-cleanups-fail-systematically\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#why-big-cleanups-fail-systematically\" title=\"Why Big Cleanups Fail Systematically\"\u003eWhy Big Cleanups Fail Systematically\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe instinctive response to accumulated technical debt is the big cleanup. A refactoring initiative. A platform rewrite. A dedicated sprint to \u0026ldquo;fix the foundation.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis approach fails for structural reasons. Maintenance requires context, and context decays quickly. The longer cleanup is delayed, the more expensive it becomes to understand what can be safely changed. Meanwhile, the product continues to evolve, reintroducing new debt in parallel.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-context-decay-beats-big-refactors\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#why-context-decay-beats-big-refactors\" title=\"Why Context Decay Beats Big Refactors\"\u003eWhy Context Decay Beats Big Refactors\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eI\u0026rsquo;ve watched teams spend entire quarters on \u0026ldquo;technical debt sprints\u0026rdquo; only to see the same problems resurface within months. The work wasn\u0026rsquo;t wrong. The timing was. By the time they got permission to clean up, they\u0026rsquo;d lost the context needed to do it efficiently.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eKehrwoche\u003c/em\u003e works precisely because it avoids this trap. The work is small enough to fit into normal life. It doesn\u0026rsquo;t require special ceremonies, approvals, or justifications. It\u0026rsquo;s simply part of living in the system.\u003c/p\u003e\n\u003cp\u003eSoftware maintenance should be treated the same way.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-boy-scout-rule-isnt-enough\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#the-boy-scout-rule-isnt-enough\" title=\"The Boy Scout Rule Isn\u0026rsquo;t Enough\"\u003eThe Boy Scout Rule Isn\u0026rsquo;t Enough\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;Leave the code better than you found it\u0026rdquo; sounds like \u003cem\u003eKehrwoche\u003c/em\u003e. In practice, it rarely functions the same way.\u003c/p\u003e\n\u003cp\u003eThe Boy Scout Rule depends on individual motivation and opportunity. It works when developers have the capacity to improve things proactively. It fails when teams are under pressure to ship features, when codebases are too complex to understand quickly, or when improvements require coordination across multiple teams.\u003c/p\u003e\n\u003cp\u003eMore fundamentally, the Boy Scout Rule makes cleanup optional and contextual. You improve things when you happen to touch them. When you have time. When it seems safe. The decision happens at the worst possible moment—when you\u0026rsquo;re already focused on something else.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve seen what this produces: teams that talk about code quality constantly but never improve it systematically. Developers who want to clean up but can\u0026rsquo;t justify the time. Pull requests that get blocked because \u0026ldquo;this change is out of scope.\u0026rdquo; The Boy Scout Rule becomes aspirational rather than operational.\u003c/p\u003e\n\u003cp\u003eThe problem isn\u0026rsquo;t lack of goodwill. It\u0026rsquo;s lack of structure. When cleanup depends on individual initiative, it competes with everything else. Feature delivery has deadlines. Bugs have severity. Technical debt has neither. It loses by default.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eKehrwoche\u003c/em\u003e doesn\u0026rsquo;t depend on motivation. It depends on assignment. You don\u0026rsquo;t clean when you feel like it. You clean when it\u0026rsquo;s your turn. That removes the decision-making burden and the social awkwardness of \u0026ldquo;wasting time\u0026rdquo; on cleanup. There\u0026rsquo;s no debate about whether cleaning is valuable. There\u0026rsquo;s only the question of whether you showed up for your assigned time.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"a-rotation-schedule-for-codebases\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#a-rotation-schedule-for-codebases\" title=\"A Rotation Schedule For Codebases\"\u003eA Rotation Schedule For Codebases\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA practical translation might look like this: Every team member gets assigned one week per quarter as their \u0026ldquo;cleanup week.\u0026rdquo; During that week, they spend their first hour each day working through a shared technical debt backlog. Not optional. Not negotiable. Just like \u003cem\u003eKehrwoche\u003c/em\u003e, everyone knows the rotation schedule. Everyone takes their turn. The rest of the team knows it\u0026rsquo;s your week and adjusts expectations accordingly.\u003c/p\u003e\n\u003cp\u003eThe items should be small enough to fit the time window. Simplify a confusing method name. Remove a dead code path. Update a misleading comment. Fix a flaky test. Add missing documentation to a cryptic function. Extract a reusable component from duplicated code. The goal isn\u0026rsquo;t heroic refactoring. It\u0026rsquo;s routine grooming.\u003c/p\u003e\n\u003cp\u003eThis shifts the question from \u0026ldquo;Should we clean up?\u0026rdquo; to \u0026ldquo;What do we clean up today?\u0026rdquo; That subtle change in framing eliminates most of the friction. Cleanup stops being a negotiation and becomes a rhythm. And just like with stairwells, your teammates notice when it\u0026rsquo;s your week. They\u0026rsquo;ll mention if nothing improved. Not aggressively—just a casual observation that carries weight.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"humor-helps-but-culture-does-the-real-work\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#humor-helps-but-culture-does-the-real-work\" title=\"Humor Helps, but Culture Does the Real Work\"\u003eHumor Helps, but Culture Does the Real Work\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSwabians complain loudly about \u003cem\u003eKehrwoche\u003c/em\u003e. There are jokes, stereotypes, and endless exaggerations about perfectionist neighbors and impossibly high cleaning standards. None of that weakens the system. If anything, it reinforces it by making the obligation visible and shared. The complaining is part of the ritual, not resistance to it.\u003c/p\u003e\n\u003cp\u003eIn software teams, humor around technical debt often serves the opposite function. It becomes a coping mechanism that replaces action. Legacy jokes turn into excuses. Irony becomes a substitute for responsibility.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve sat through retrospectives where teams laugh about the \u0026ldquo;haunted module\u0026rdquo; nobody dares to touch. Everyone agrees it\u0026rsquo;s a problem. Everyone acknowledges someone should fix it. Then the meeting ends, and nothing changes. The joke releases tension without creating obligation.\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;We\u0026rsquo;ll fix it in the next sprint\u0026rdquo; becomes the technical debt equivalent of \u0026ldquo;thoughts and prayers.\u0026rdquo; It signals concern without committing action. The laughter acknowledges shared pain but doesn\u0026rsquo;t demand shared responsibility.\u003c/p\u003e\n\u003cp\u003eThe difference is structural. In Swabia, you can complain about \u003cem\u003eKehrwoche\u003c/em\u003e all you want. You still have to clean when it\u0026rsquo;s your week. The humor doesn\u0026rsquo;t grant an exemption. In software, humor often becomes the exemption itself. We laugh instead of fixing.\u003c/p\u003e\n\u003cp\u003eA healthy culture allows humor without letting it undermine discipline. Complaining is fine. Avoiding cleanup is not. The question is whether your jokes postpone work or acknowledge the work you\u0026rsquo;re already doing.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"a-practical-takeaway\"\u003e\u003ca href=\"/posts/kehrwoche-technical-debt/#a-practical-takeaway\" title=\"A Practical Takeaway\"\u003eA Practical Takeaway\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003eKehrwoche\u003c/em\u003e scales because it\u0026rsquo;s boring, predictable, and non-negotiable. It doesn\u0026rsquo;t rely on heroics or passion. It relies on consistency and social expectation. That\u0026rsquo;s the part most software teams get wrong. They wait for inspiration, alignment, or the perfect moment. None of those ever arrive.\u003c/p\u003e\n\u003cp\u003eApplied to software development, this means treating technical debt as a first-class operational concern. Cleanup must be small, frequent, and attached to real ownership. Not a future initiative. Not a side project. Not something that only happens when everything else is done.\u003c/p\u003e\n\u003cp\u003eStart with the rotation. Assign cleanup weeks to team members on a predictable schedule. Make it visible. Make it normal. Make it expected. When it\u0026rsquo;s your week, you clean. When it\u0026rsquo;s not, you respect that someone else is handling the work you\u0026rsquo;ll do next quarter.\u003c/p\u003e\n\u003cp\u003eThe work itself should be unglamorous. If it feels heroic, you\u0026rsquo;ve waited too long. Simplify method names. Remove dead imports. Fix misleading comments. Update outdated documentation. Delete code paths nobody uses anymore. These aren\u0026rsquo;t the changes that go in blog posts. They\u0026rsquo;re the changes that keep the system navigable.\u003c/p\u003e\n\u003cp\u003eClean systems aren\u0026rsquo;t the result of exceptional engineers. They\u0026rsquo;re the result of ordinary engineers doing unglamorous work regularly. That\u0026rsquo;s the uncomfortable truth \u003cem\u003eKehrwoche\u003c/em\u003e makes unavoidable.\u003c/p\u003e\n\u003cp\u003eIn Swabia, that work involves a broom.\u003c/p\u003e\n\u003cp\u003eIn software, it involves discipline, restraint, and the willingness to clean up after yourself before someone else has to.\u003c/p\u003e\n\u003cp\u003eThe stairwell is shared. Everyone walks through it. Everyone keeps it clean. Your turn comes around whether you like it or not. The only question is whether you\u0026rsquo;ll show up.\u003c/p\u003e\n\u003cp\u003eAnd if you need a place to start sweeping—that \u003cem\u003e4,000-line\u003c/em\u003e \u003ccode\u003eUtils.cs\u003c/code\u003e file everyone\u0026rsquo;s afraid to touch is basically the digital equivalent of a stairwell that hasn\u0026rsquo;t seen a broom in three years. Mrs. Schmid would be \u003cstrong\u003edisappointed\u003c/strong\u003e.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-09T11:00:00+01:00","id":"https://daily-devops.net/posts/kehrwoche-technical-debt/","language":"en","summary":"A Swabian tradition reveals why small, routine maintenance beats big cleanup initiatives—and what software teams get wrong about technical debt.\n","tags":["technicaldebt","softwareengineering","codequality","bestpractices"],"title":"Kehrwoche: What Swabian Cleaning Teaches About Technical Debt","url":"https://daily-devops.net/posts/kehrwoche-technical-debt/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\n\n\n\n\u003ch2 id=\"your-tuesday-morning-a-true-story\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#your-tuesday-morning-a-true-story\" title=\"Your Tuesday Morning: A True Story\"\u003eYour Tuesday Morning: A True Story\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIt\u0026rsquo;s 9 AM. You\u0026rsquo;re debugging why the Kubernetes deployment failed overnight. The YAML looked perfect. Indentation? Check. Syntax? Check. The problem? Someone used \u003ccode\u003eNO\u003c/code\u003e as an environment variable value. YAML helpfully parsed it as boolean \u003ccode\u003efalse\u003c/code\u003e. Your pod never started.\u003c/p\u003e\n\u003cp\u003eBy 10 AM, you\u0026rsquo;re fixing the CSV export that accounting requested. Excel mangled the employee IDs—turned \u003ccode\u003e00123\u003c/code\u003e into \u003ccode\u003e123\u003c/code\u003e, converted the date column into something unrecognizable, and decided that gene names like \u003ccode\u003eSEPT2\u003c/code\u003e are obviously September 2nd.\u003c/p\u003e\n\u003cp\u003eAt 11 AM, the build pipeline breaks because someone added a trailing comma to \u003ccode\u003eappsettings.json\u003c/code\u003e. JSON doesn\u0026rsquo;t allow those. The error message is cryptic. The fix takes 30 seconds. Finding it took 20 minutes.\u003c/p\u003e\n\u003cp\u003eLunch is spent explaining to a junior dev why we have YAML for CI/CD, JSON for app config, TOML for the Rust tool, INI for the legacy service, and CSV for data exports. \u0026ldquo;Can\u0026rsquo;t we just pick one?\u0026rdquo; they ask.\u003c/p\u003e\n\u003cp\u003eNo. We can\u0026rsquo;t. This is software development in 2025.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-format-parade-whos-who-in-the-chaos\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#the-format-parade-whos-who-in-the-chaos\" title=\"The Format Parade: Who\u0026rsquo;s Who in the Chaos\"\u003eThe Format Parade: Who\u0026rsquo;s Who in the Chaos\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s meet the contestants in this never-ending format beauty pageant. Spoiler: nobody wins.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"csv-the-universal-disaster\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#csv-the-universal-disaster\" title=\"CSV: The Universal Disaster\"\u003e\u003cstrong\u003eCSV: The Universal Disaster\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eComma-Separated Values promised simplicity: rows, columns, commas. That\u0026rsquo;s it.\u003c/p\u003e\n\u003cp\u003eThe reality? There\u0026rsquo;s no real standard. RFC 4180 tried in 2005, but thousands of tools had already shipped their own interpretations. Comma delimiter? Sometimes semicolon. Sometimes tab. Quote your strings? Maybe. Escape quotes by doubling them? Or backslashes? Depends on the tool.\u003c/p\u003e\n\u003cp\u003eExcel is CSV\u0026rsquo;s natural enemy. It will \u0026ldquo;fix\u0026rdquo; your data by converting dates (\u003ccode\u003e2025-12-31\u003c/code\u003e becomes Excel\u0026rsquo;s internal date format), stripping leading zeros (\u003ccode\u003e00123\u003c/code\u003e → \u003ccode\u003e123\u003c/code\u003e), and famously turning gene names into dates (\u003ccode\u003eSEPT2\u003c/code\u003e → Sep 2). Biologists have a special hatred for CSV because of this.\u003c/p\u003e\n\u003cp\u003eYet CSV survives because it\u0026rsquo;s \u003cstrong\u003euniversal\u003c/strong\u003e. Every tool exports it. Every developer can edit it in Notepad. It compresses well. It\u0026rsquo;s the lowest common denominator when nothing else works.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Like democracy, it\u0026rsquo;s the worst format except for all the others.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"ini-the-minimalist\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#ini-the-minimalist\" title=\"INI: The Minimalist\u0026rsquo;s Dream\"\u003e\u003cstrong\u003eINI: The Minimalist\u0026rsquo;s Dream\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eKey-value pairs. Sections. That\u0026rsquo;s the entire spec. Humans understand it instantly.\u003c/p\u003e\n\u003cp\u003eThe problem? No nested structures. No lists. No type system—everything\u0026rsquo;s a string. The moment you need hierarchy, INI taps out.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Perfect for simple configs. Useless for everything else.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"xml-the-enterprise-albatross\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#xml-the-enterprise-albatross\" title=\"XML: The Enterprise Albatross\"\u003e\u003cstrong\u003eXML: The Enterprise Albatross\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eXML promised schema validation, namespaces, self-describing tags—enterprise-grade power.\u003c/p\u003e\n\u003cp\u003eWhat we got: angle bracket hell. Every value wrapped in opening and closing tags. Signal-to-noise ratio so poor that even enterprise architects—people who \u003cem\u003ethrive\u003c/em\u003e on complexity—started looking for alternatives.\u003c/p\u003e\n\u003cp\u003eWhen the people who love complexity want simpler, you\u0026rsquo;ve failed at usability.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Still haunting legacy systems. Nobody voluntarily starts new projects with XML anymore.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"json-the-machine\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#json-the-machine\" title=\"JSON: The Machine\u0026rsquo;s Format\"\u003e\u003cstrong\u003eJSON: The Machine\u0026rsquo;s Format\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eJSON solved XML\u0026rsquo;s verbosity. Clean syntax. Maps directly to data structures. Every language has a parser.\u003c/p\u003e\n\u003cp\u003eBut it was designed for \u003cstrong\u003emachines\u003c/strong\u003e, not humans. No comments (explain your config changes in commit messages, I guess). Trailing commas forbidden. Rigid quoting everywhere. It works, but it\u0026rsquo;s tedious to hand-edit.\u003c/p\u003e\n\u003cp\u003eOpenAI\u0026rsquo;s function calling? JSON-only. Why? Because it\u0026rsquo;s deterministic. One correct way to structure it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Boring, reliable, ubiquitous. The Toyota Camry of data formats.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"yaml-the-beautiful-disaster\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#yaml-the-beautiful-disaster\" title=\"YAML: The Beautiful Disaster\"\u003e\u003cstrong\u003eYAML: The Beautiful Disaster\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYAML threw JSON\u0026rsquo;s rigidity out the window. Minimal syntax. Comments everywhere. Indentation-based. Human-friendly!\u003c/p\u003e\n\u003cp\u003eExcept it\u0026rsquo;s \u003cstrong\u003ewhitespace-sensitive\u003c/strong\u003e. One misaligned space breaks everything, often silently. Implicit type conversions bite constantly (\u003ccode\u003eNO\u003c/code\u003e → \u003ccode\u003efalse\u003c/code\u003e, \u003ccode\u003eon\u003c/code\u003e → \u003ccode\u003etrue\u003c/code\u003e, \u003ccode\u003e0123\u003c/code\u003e → octal 83). The spec is 23,000 words and allows multiple ways to represent the same data.\u003c/p\u003e\n\u003cp\u003eKubernetes chose YAML. Docker Compose chose YAML. GitHub Actions chose YAML. The ecosystem standardized, so now you\u0026rsquo;re learning YAML whether you like it or not.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Feels great until it doesn\u0026rsquo;t. Then you\u0026rsquo;re debugging indentation at 2 AM.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"toml-the-pragmatic-middle-ground\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#toml-the-pragmatic-middle-ground\" title=\"TOML: The Pragmatic Middle Ground\"\u003e\u003cstrong\u003eTOML: The Pragmatic Middle Ground\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTom\u0026rsquo;s Obvious Minimal Language tried to be INI + structure + types. Explicit syntax. No whitespace sensitivity. Comments allowed.\u003c/p\u003e\n\u003cp\u003eIt works. It\u0026rsquo;s clear. It\u0026rsquo;s unambiguous. The ecosystem is smaller than YAML/JSON, but growing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Underrated. Use it for build configs if you can.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"taml-radical-minimalism\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#taml-radical-minimalism\" title=\"TAML: Radical Minimalism\"\u003e\u003cstrong\u003eTAML: Radical Minimalism\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTab Annotated Markup Language: tabs for hierarchy, newlines for structure. No brackets, colons, or quotes.\u003c/p\u003e\n\u003cp\u003eOne tab = one level deeper. That\u0026rsquo;s the entire format.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Interesting experiment. Tiny ecosystem. Good for greenfield projects if you\u0026rsquo;re willing to bet on niche formats.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"toon-designed-for-ai\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#toon-designed-for-ai\" title=\"TOON: Designed for AI\"\u003e\u003cstrong\u003eTOON: Designed for AI\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eToken-Oriented Object Notation emerged in 2025 specifically to solve LLM generation problems.\u003c/p\u003e\n\u003cp\u003e~40% fewer tokens than JSON. Explicit \u003ccode\u003e[N]\u003c/code\u003e array lengths and \u003ccode\u003e{fields}\u003c/code\u003e headers give AI models clear guardrails. Better accuracy (74% vs JSON\u0026rsquo;s 70%) because the schema is baked into the syntax.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s \u0026ldquo;JSON optimized for transformer models.\u0026rdquo; Human-readable like YAML, compact like CSV, schema-aware like XML.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e If you\u0026rsquo;re building systems where LLMs frequently generate structured data, TOON might save you. Otherwise, wait to see if it gains traction.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"ccl-category-theory-elegance\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#ccl-category-theory-elegance\" title=\"CCL: Category Theory Elegance\"\u003e\u003cstrong\u003eCCL: Category Theory Elegance\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCategorical Configuration Language built on mathematical principles. Pure key-value pairs with recursive nesting.\u003c/p\u003e\n\u003cp\u003eMinimal syntax: \u003ccode\u003ekey = value\u003c/code\u003e. Comments via \u003ccode\u003e/=\u003c/code\u003e. Merging configs is associative with an identity element. Provably correct composition.\u003c/p\u003e\n\u003cp\u003eThe ecosystem is tiny (OCaml, Rust implementations). Practical? Depends on whether you value theoretical soundness over ecosystem maturity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e For people who think in category theory. Everyone else, use TOML.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"bson-the-binary-option\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#bson-the-binary-option\" title=\"BSON: The Binary Option\"\u003e\u003cstrong\u003eBSON: The Binary Option\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBinary JSON. Optimized for machine efficiency. Fast parsing. Compact storage.\u003c/p\u003e\n\u003cp\u003eOpen it in a text editor? Gibberish. It\u0026rsquo;s for databases (MongoDB), not human editing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Right tool for the right job. Don\u0026rsquo;t hand-edit BSON.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-we-cant-have-nice-things\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#why-we-cant-have-nice-things\" title=\"Why We Can\u0026rsquo;t Have Nice Things\"\u003eWhy We Can\u0026rsquo;t Have Nice Things\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s the uncomfortable truth: \u003cstrong\u003eevery format genuinely solved real problems\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eCSV ended proprietary spreadsheet lock-in. XML brought schema validation. JSON fixed XML\u0026rsquo;s verbosity. YAML made configs readable. TOML removed YAML\u0026rsquo;s gotchas. TAML went minimal. TOON optimized for AI. CCL brought mathematical rigor.\u003c/p\u003e\n\u003cp\u003eEach improvement was real. Each one also created new problems.\u003c/p\u003e\n\u003cp\u003eThe xkcd comic about competing standards isn\u0026rsquo;t a joke anymore—it\u0026rsquo;s your job description. We don\u0026rsquo;t have 15 standards. We have 50. Maybe 100.\u003c/p\u003e\n\u003cp\u003eFormats don\u0026rsquo;t converge because \u003cstrong\u003etrade-offs are real\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHuman readability ↔ Machine efficiency\u003c/li\u003e\n\u003cli\u003eFlexibility ↔ Parsability\u003c/li\u003e\n\u003cli\u003eSimplicity ↔ Features\u003c/li\u003e\n\u003cli\u003eEcosystem size ↔ Specialized optimization\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAs AI systems join the picture, the calculus shifts again. Formats designed for humans sometimes hurt AI. Formats designed for machines frustrate humans. Designing for both? That\u0026rsquo;s what TOON attempts.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-ai-problem-makes-everything-worse\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#the-ai-problem-makes-everything-worse\" title=\"The AI Problem Makes Everything Worse\"\u003eThe AI Problem Makes Everything Worse\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLarge language models changed the game.\u003c/p\u003e\n\u003cp\u003eChatGPT generates text token by token via pattern matching. Rigid formats like JSON? Manageable. Structure it one of 3-4 ways, models usually succeed.\u003c/p\u003e\n\u003cp\u003eYAML? Disaster. Whitespace sensitivity, implicit type conversions, multiple valid representations—LLMs generate frequently invalid YAML. The \u003cem\u003estructure\u003c/em\u003e looks right, but subtle indentation errors or quoting mistakes break parsing.\u003c/p\u003e\n\u003cp\u003eThis drove OpenAI to mandate JSON-only for function calling. Not because engineers are lazy. Because JSON has \u003cstrong\u003eone correct way\u003c/strong\u003e, and models can learn it reliably.\u003c/p\u003e\n\u003cp\u003eThe irony: we designed YAML for human convenience. AI exposed that \u0026ldquo;flexibility\u0026rdquo; creates too many ways to fail.\u003c/p\u003e\n\u003cp\u003eTOON exists specifically to solve this. Explicit schema headers, deterministic structure, fewer tokens. It\u0026rsquo;s pragmatic engineering: \u0026ldquo;YAML breaks AI, JSON works but is verbose, so let\u0026rsquo;s design something in between that models can generate correctly.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"survival-guide-five-strategies-that-actually-work\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#survival-guide-five-strategies-that-actually-work\" title=\"Survival Guide: Five Strategies That Actually Work\"\u003eSurvival Guide: Five Strategies That Actually Work\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;re stuck with this chaos. Here\u0026rsquo;s how to survive it:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1. Choose Deliberately\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re using JSON for config, \u003cstrong\u003eown it\u003c/strong\u003e. Document the decision. Set up schema validation. If you\u0026rsquo;re exporting CSV, specify delimiter, encoding, quoting rules explicitly.\u003c/p\u003e\n\u003cp\u003eDon\u0026rsquo;t drift into formats by accident. Make conscious choices and commit to them.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Invest in Tooling\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eLinters. Schema validators. Type safety. These matter \u003cstrong\u003emore than the format\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eGood tooling makes mediocre formats manageable:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCSV: parsers with RFC 4180 support\u003c/li\u003e\n\u003cli\u003eJSON: JSON Schema validators\u003c/li\u003e\n\u003cli\u003eYAML: linters that catch indentation issues\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e3. Be Skeptical of \u0026ldquo;Revolutionary\u0026rdquo; Formats\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eEvery new format promises to be The One. TOON and CCL might be useful for specific niches (LLM generation, category theory), but they\u0026rsquo;re not replacing JSON tomorrow.\u003c/p\u003e\n\u003cp\u003eEvaluate pragmatically. Bet on ecosystems, not elegance.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4. Plan for AI Integration\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIf AI generates structured data in your system:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSafe choice:\u003c/strong\u003e JSON with aggressive validation\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExperimental:\u003c/strong\u003e TOON if you\u0026rsquo;re adopting early-stage formats\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAvoid:\u003c/strong\u003e YAML unless you enjoy debugging AI-generated indentation errors\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e5. Don\u0026rsquo;t Refactor Just to Standardize\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eLegacy system using INI, XML, or CSV? If it works, \u003cstrong\u003eleave it alone\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eRefactoring formats doesn\u0026rsquo;t fix business problems. It creates migration risk. Only change formats when you\u0026rsquo;re solving actual pain, not pursuing theoretical purity.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"living-with-the-chaos\"\u003e\u003ca href=\"/posts/alphabet-soup-file-formats/#living-with-the-chaos\" title=\"Living with the Chaos\"\u003eLiving with the Chaos\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe light at the end of the tunnel isn\u0026rsquo;t format convergence. It never was.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s accepting that we\u0026rsquo;ll always have multiple formats. CSV for exports. JSON for APIs. YAML for infrastructure. TOML for builds. Maybe TOON for AI outputs.\u003c/p\u003e\n\u003cp\u003eThe real problem was never the format. It was always the \u003cstrong\u003edata\u003c/strong\u003e—complex, context-dependent, requiring human judgment.\u003c/p\u003e\n\u003cp\u003ePick the right tool for each job. Invest in validation. Build good tooling. Stay skeptical of salvation promises.\u003c/p\u003e\n\u003cp\u003eWelcome to file format hell. You\u0026rsquo;re going to be here a while.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-08T17:00:00+01:00","id":"https://daily-devops.net/posts/alphabet-soup-file-formats/","language":"en","summary":"CSV breaks on commas. YAML breaks on spaces. JSON breaks on trailing commas. TOML, TAML, TOON, CCL joined the chaos. Nobody wins. Here's why.\n","tags":["configuration","devops","dotnet","bestpractices","softwareengineering","codequality"],"title":"Alphabet Soup: The Format Buffet Nobody Ordered\n","url":"https://daily-devops.net/posts/alphabet-soup-file-formats/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA \u003ca href=\"https://www.linkedin.com/posts/davideguida_i-feel-its-time-to-make-something-clear-activity-7411391768283271168-naE4\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eLinkedIn post by David Guida\u003c/a\u003e sparked a discussion that cuts to the bone: \u003cstrong\u003eIs software engineering about thinking or typing?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eDavid argued forcefully that \u0026ldquo;software engineering is NOT about writing code\u0026rdquo;—that code is merely mechanical output, the easy part, just another language. The hard part, he wrote, is thinking: \u0026ldquo;Programming is a byproduct of the thinking process. And that one, my friends, is the hard part.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eI responded with a point that needed more space than a LinkedIn comment allows:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Strong point, but it slightly overcorrects. Yes, typing code is the easy, mechanical part. The hard part is reasoning, trade-offs, and understanding constraints. Agreed. \u003cstrong\u003eBut dismissing code as \u0026lsquo;just another language\u0026rsquo; undersells its impact.\u003c/strong\u003e Code is not only expression, it is execution, cost, failure modes, and long-term operational risk. Thinking without being forced into precise, executable form often stays vague. Writing code is where weak thinking gets exposed. Programming is a byproduct of thinking, \u003cstrong\u003ebut it is also the feedback loop that sharpens that thinking.\u003c/strong\u003e One without the other does not scale.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eDavid\u0026rsquo;s response captured what I\u0026rsquo;m exploring here: \u0026ldquo;I totally agree! I must have oversimplified my thoughts. Your closing note on the feedback loop \u003cstrong\u003ecaptures the reason why real professionals will never be replaced.\u003c/strong\u003e\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat last phrase wasn\u0026rsquo;t casual. It addresses the elephant everyone sees but few acknowledge: \u003cstrong\u003eAI code assistants everywhere.\u003c/strong\u003e GitHub Copilot. ChatGPT generating entire applications from prompts. The emerging \u0026ldquo;vibe coding\u0026rdquo; trend where developers describe vibes and let AI handle the dirty work.\u003c/p\u003e\n\u003cp\u003eThe timing matters. We\u0026rsquo;re in an era where typing code has \u003cstrong\u003enever been easier\u003c/strong\u003e. AI generates syntactically correct implementations \u003cstrong\u003efaster than any human can type\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eYet here\u0026rsquo;s what David and I both realized: This makes the feedback loop between thinking and code \u003cstrong\u003emore critical, not less\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAsk yourself:\u003c/strong\u003e When code generation becomes trivial, what separates you from a prompt engineer who thinks they\u0026rsquo;re building software?\u003c/p\u003e\n\u003cp\u003eThe answer: Understanding what that code actually \u003cstrong\u003edoes\u003c/strong\u003e. What it \u003cstrong\u003ecosts\u003c/strong\u003e. Where it \u003cstrong\u003efails\u003c/strong\u003e. Why it \u003cstrong\u003ebreaks under load\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThis article expands on that feedback loop—the relationship between thinking and code that AI can\u0026rsquo;t replicate. It explores why AI-generated code without deep understanding creates an \u003cstrong\u003eillusion of productivity that collapses catastrophically under production load\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-vague-thinking-hides\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#where-vague-thinking-hides\" title=\"Where Vague Thinking Hides\"\u003eWhere Vague Thinking Hides\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWalk through any architecture review where diagrams look perfect, responsibilities seem clear, and everyone nods in agreement. Then watch what happens when someone starts writing the actual implementation. Suddenly, the clean boundaries blur. The \u0026ldquo;simple\u0026rdquo; abstraction requires five parameters. The proposed interface doesn\u0026rsquo;t fit half the use cases. The design that felt obvious in discussion becomes ambiguous when translated to executable code.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t implementation failing design. This is design revealing itself to be incomplete.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve sat through countless discussions where proposed solutions felt reasonable until we asked: \u0026ldquo;Show me the code.\u0026rdquo; Not production code—just a sketch. Suddenly, implicit assumptions surface. Missing responsibilities become visible. Performance implications emerge. The architecture that seemed solid in abstract terms crumbles when forced into compilable form.\u003c/p\u003e\n\u003cp\u003eCode demands precision that thought alone doesn\u0026rsquo;t require. When you think through a problem, your mind fills gaps unconsciously, papers over inconsistencies, and substitutes intuition for rigor. When you write code, the compiler—and eventually production—refuses to cooperate with vague intent.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-the-compiler-exposes-vague-thinking\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#how-the-compiler-exposes-vague-thinking\" title=\"How The Compiler Exposes Vague Thinking\"\u003eHow The Compiler Exposes Vague Thinking\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsider nullable reference types in C#. Without explicit declaration, you can mentally handwave nullability concerns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Vague thinking: \u0026#34;customer will always have a name\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEnable nullable reference types, and the compiler forces you to confront reality:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Warning CS8618: Non-nullable property \u0026#39;Name\u0026#39; must contain \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                                      \u003cspan class=\"c1\"\u003e// a non-null value when exiting constructor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis isn\u0026rsquo;t pedantry. This is thinking being forced into honest, executable form. Either you guarantee initialization, accept nullability explicitly, or redesign the constructor contract:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe act of writing code exposed a decision that pure thought glossed over. Was \u003ccode\u003eName\u003c/code\u003e required or optional? The compiler didn\u0026rsquo;t care about your mental model—it demanded an explicit answer.\u003c/p\u003e\n\u003cp\u003eThis happens at every level: API contracts, concurrency assumptions, resource ownership, error propagation. Abstract thinking lets you defer these decisions indefinitely. Code forces resolution.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-vibe-coding-illusion-when-ai-generates-code-without-understanding\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#the-vibe-coding-illusion-when-ai-generates-code-without-understanding\" title=\"The Vibe Coding Illusion: When AI Generates Code Without Understanding\"\u003eThe Vibe Coding Illusion: When AI Generates Code Without Understanding\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAI code assistants accelerate the mechanical part—the typing—\u003cstrong\u003eextraordinarily well\u003c/strong\u003e. Describe a function in natural language, and GitHub Copilot suggests an implementation within seconds. Ask ChatGPT to build a REST API, and it generates hundreds of lines of code that compile and often run.\u003c/p\u003e\n\u003cp\u003eThis feels like \u003cstrong\u003emagic\u003c/strong\u003e until you ask the critical question: \u003cem\u003edoes the generated code do what you actually need, not what you described?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve reviewed pull requests where developers used AI to generate complete features. The code compiled. Tests passed. The PR description matched the implementation. Everything looked \u003cstrong\u003efine\u003c/strong\u003e. Until production deployment revealed that the AI had:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGenerated \u003cstrong\u003ethread-unsafe code\u003c/strong\u003e for concurrent scenarios the prompt didn\u0026rsquo;t mention\u003c/li\u003e\n\u003cli\u003eAllocated memory in hot paths \u003cstrong\u003ewithout consideration\u003c/strong\u003e for garbage collection pressure\u003c/li\u003e\n\u003cli\u003eImplemented \u003cstrong\u003eO(n²) algorithms\u003c/strong\u003e where O(n) solutions existed\u003c/li\u003e\n\u003cli\u003eCreated database queries that worked with test data but \u003cstrong\u003efailed catastrophically\u003c/strong\u003e with production scale\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIgnored error handling\u003c/strong\u003e edge cases that weren\u0026rsquo;t in the prompt\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eThe AI didn\u0026rsquo;t fail—it did exactly what was asked.\u003c/strong\u003e The developer failed by not understanding that code generated from a prompt is a starting point, \u003cstrong\u003enot a solution\u003c/strong\u003e. The feedback loop—write code, measure behavior, understand consequences, refine thinking—got \u003cstrong\u003eshort-circuited\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-vibe-coding-collapses-under-load\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#why-vibe-coding-collapses-under-load\" title=\"Why Vibe Coding Collapses Under Load\"\u003eWhy Vibe Coding Collapses Under Load\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u0026ldquo;\u003cstrong\u003eVibe coding\u003c/strong\u003e\u0026rdquo; is the term emerging for this pattern: describe the vibe of what you want, let AI generate implementation, ship it if it passes basic tests. It treats code as expression divorced from execution reality. It assumes that if code compiles and handles the happy path, \u003cstrong\u003eit\u0026rsquo;s correct\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThis assumption works until it doesn\u0026rsquo;t.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAnd when it doesn\u0026rsquo;t? You\u0026rsquo;re stuck. No foundation for debugging. Can\u0026rsquo;t reason about performance. Can\u0026rsquo;t identify where implementation diverges from requirements. Can\u0026rsquo;t refactor intelligently.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhy?\u003c/strong\u003e Because you don\u0026rsquo;t understand what the code actually does.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHere\u0026rsquo;s your professional advantage:\u003c/strong\u003e You recognize what the compiler \u003cstrong\u003ecan\u0026rsquo;t\u003c/strong\u003e tell you:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThat the generated code works for the described case but \u003cstrong\u003efails for the dozen edge cases\u003c/strong\u003e you didn\u0026rsquo;t think to mention\u003c/li\u003e\n\u003cli\u003eThat the algorithm performs acceptably with 100 records but \u003cstrong\u003ecollapses with 100,000\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eThat the abstraction looks clean but \u003cstrong\u003ecreates maintenance nightmares\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eAI generates syntax. Professionals understand semantics, performance characteristics, failure modes, and operational implications.\u003c/strong\u003e The gap between these is where \u0026ldquo;real professionals will never be replaced.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"code-materializes-consequences\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#code-materializes-consequences\" title=\"Code Materializes Consequences\"\u003eCode Materializes Consequences\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCode isn\u0026rsquo;t just structured thought—it\u0026rsquo;s thought with operational consequences. When you design an architecture, you\u0026rsquo;re reasoning about responsibilities and boundaries. When you implement it, you\u0026rsquo;re creating CPU consumption patterns, memory allocation profiles, I/O bottlenecks, and long-term maintenance burdens.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t secondary concerns. They\u0026rsquo;re the actual impact of your decisions.\u003c/p\u003e\n\u003cp\u003eTake a straightforward example: caching. In discussion, caching sounds simple—\u0026ldquo;we\u0026rsquo;ll cache frequently accessed data.\u0026rdquo; The thinking feels complete. Then you implement it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Looks reasonable in isolation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLoad\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis code compiles. It runs. It even passes basic functional tests.\u003c/p\u003e\n\u003cp\u003eThen production hits.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-abstract-thinking-defers\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#what-abstract-thinking-defers\" title=\"What Abstract Thinking Defers\"\u003eWhat Abstract Thinking Defers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eWhat abstract thinking missed:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMemory\u003c/strong\u003e: Cache grows unbounded. No eviction policy. Memory consumption increases until the process crashes or triggers garbage collection storms that degrade response times by 300%.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConcurrency\u003c/strong\u003e: No synchronization. Multiple threads corrupt dictionary state, causing crashes or silent data corruption that costs hours of incident response time.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConsistency\u003c/strong\u003e: Cache never invalidates. Stale data persists indefinitely, creating subtle bugs that customer support escalates—costing reputation and revenue.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eObservability\u003c/strong\u003e: No metrics. You can\u0026rsquo;t tell if caching helps or hurts performance without instrumenting separately.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEach of these issues represents thinking that felt complete in abstract terms but was fundamentally incomplete in executable reality. The \u0026ldquo;simple\u0026rdquo; caching decision materialized as:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eConcurrentDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCacheEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e \u003cspan class=\"n\"\u003e_expirationWindow\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_maxCacheSize\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"p\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsExpired\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_expirationWindow\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eMetrics\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheHitCounter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIncrement\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eentry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eMetrics\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheMissCounter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIncrement\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLoad\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_maxCacheSize\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eEvictOldestEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCacheEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTimeOffset\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCode didn\u0026rsquo;t complicate a simple idea—it revealed that the idea was never actually simple. Abstract thinking deferred decisions about memory, concurrency, staleness, observability, and eviction. Code forced those decisions into concrete form where consequences become visible and measurable.\u003c/p\u003e\n\u003cp\u003eThis is not implementation detail obscuring elegant design. This is reality asserting itself.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-comes-next-the-feedback-loop-ai-cannot-replicate\"\u003e\u003ca href=\"/posts/code-sharpens-thinking/#what-comes-next-the-feedback-loop-ai-cannot-replicate\" title=\"What Comes Next: The Feedback Loop AI Cannot Replicate\"\u003eWhat Comes Next: The Feedback Loop AI Cannot Replicate\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAI-generated code without understanding creates productivity illusions that collapse in production. Code forces abstract thinking into executable form, exposing gaps that pure reasoning glosses over. That much is clear.\u003c/p\u003e\n\u003cp\u003eBut understanding the problem doesn\u0026rsquo;t answer the deeper question: What exactly is this feedback loop between code and reality, and why can\u0026rsquo;t AI replicate it? What mechanisms transform vague reasoning into concrete understanding?\u003c/p\u003e\n\u003cp\u003eThe answer lies in the tools we use every day: compilers, profilers, tests, production environments. These aren\u0026rsquo;t just validation gates. They\u0026rsquo;re \u003cstrong\u003ereality engines\u003c/strong\u003e that do something AI fundamentally cannot: they execute your assumptions against actual constraints and report back with unfiltered truth.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eIn the next part of this series, we\u0026rsquo;ll explore how these mechanisms form a cognitive feedback loop that sharpens professional thinking in ways no AI prompt can simulate.\u003c/em\u003e\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-06T17:00:00+01:00","id":"https://daily-devops.net/posts/code-sharpens-thinking/","language":"en","summary":"Typing code is trivial now—AI does it instantly. So why will real professionals never be replaced? Because vibe coding collapses under production reality.\n","tags":["softwareengineering","codequality","bestpractices","architecture","dotnet","csharp","technicaldebt","ai-code-assistant","github-copilot"],"title":"Why Real Professionals Will Never Be Replaced by AI\n","url":"https://daily-devops.net/posts/code-sharpens-thinking/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003eHappy New Year 2026! 🎉\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eSkip the generic wishes. My wish: fix the technical debt you\u0026rsquo;ve been promising since 2023. Stop telling yourself it will happen \u003cem\u003enext quarter.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eEvery January, the same ritual. Sprint planning. Someone mentions that problematic module—you know the one. \u0026ldquo;We\u0026rsquo;ll refactor it next quarter,\u0026rdquo; they say. Ticket created. Backlog updated.\u003c/p\u003e\n\u003cp\u003eBy mid-January, forgotten.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve taught enough .NET courses and consulted with enough teams to know: Everyone has technical debt. The Fortune 500 companies have it. The startups have it. You have it. The difference between teams that succeed in 2026 and teams that burn out isn\u0026rsquo;t whether they have technical debt—it\u0026rsquo;s whether they\u0026rsquo;re honest about it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-next-quarter-never-comes\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#why-next-quarter-never-comes\" title=\"Why Next Quarter Never Comes\"\u003eWhy \u003cem\u003eNext Quarter\u003c/em\u003e Never Comes\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern is always the same. A feature ships. It works—barely. \u0026ldquo;We\u0026rsquo;ll clean this up next quarter,\u0026rdquo; someone says. The team knows it\u0026rsquo;s a lie. Management knows it\u0026rsquo;s a lie. Everyone pretends anyway.\u003c/p\u003e\n\u003cp\u003eWhy? Because admitting you\u0026rsquo;re building on a foundation of compromises feels like failure. It\u0026rsquo;s not. It\u0026rsquo;s reality. But we\u0026rsquo;d rather maintain the fiction.\u003c/p\u003e\n\u003cp\u003eTemporary solutions become permanent infrastructure. That \u0026ldquo;quick integration\u0026rdquo; from 2019 is now mission-critical and touches everything. The developer who wrote it left in 2021. The documentation? Nonexistent.\u003c/p\u003e\n\u003cp\u003eThis compounds. Every shortcut builds on the previous shortcut. Every \u0026ldquo;we\u0026rsquo;ll fix it later\u0026rdquo; adds to the pile. Fast forward to 2026, and you\u0026rsquo;re spending more time working around bad decisions than you would have spent making good ones.\u003c/p\u003e\n\u003cp\u003eTechnical debt feels free when you create it. Your sprint metrics look great. You shipped the feature. Everyone\u0026rsquo;s happy.\u003c/p\u003e\n\u003cp\u003eEighteen months later, that code is load-bearing. Developers quit because debugging incomprehensible code is soul-crushing. Your velocity drops. The business wonders why. You can\u0026rsquo;t admit the foundation is crumbling.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"every-january-same-promises\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#every-january-same-promises\" title=\"Every January, Same Promises\"\u003eEvery January, Same Promises\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI teach .NET courses for students and apprentices. Every year, developers tell me about the refactoring they\u0026rsquo;re planning.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cem\u003eThis year we\u0026rsquo;ll finally add tests.\u003c/em\u003e\u003c/li\u003e\n\u003cli\u003e\u003cem\u003eThis year we\u0026rsquo;ll upgrade from .NET Framework 4.8.\u003c/em\u003e\u003c/li\u003e\n\u003cli\u003e\u003cem\u003eThis year we\u0026rsquo;ll split up that 4,000-line controller.\u003c/em\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBy February, they\u0026rsquo;re back to shipping features. The tests? Still at 12% coverage. The Framework migration? Still \u0026ldquo;risky.\u0026rdquo; The controller? Now 4,300 lines.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the code everyone writes on January 1st:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderProcessor\u003c/span\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2023: Refactor this - too much responsibility\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2024: Seriously, we need to split this up\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2025: I\u0026#39;m not even joking anymore\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// TODO 2026: Kill me\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderState\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_stateCache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Race condition central\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e// CA2007 warning since 2022, still ignored\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Checking null in 2026 like it\u0026#39;s 2015\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSaveToDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Deadlock count: 23 and climbing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSendEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// NullReferenceException #47 this month\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThose CA2007 warnings from ConfigureAwait? Been there since you upgraded to .NET Core 3.1 in 2020. You keep meaning to fix them. You suppress them instead because \u0026ldquo;we\u0026rsquo;ll do it properly in the refactoring.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t developer failure. It\u0026rsquo;s organizational reality. You can only refactor when the business prioritizes it (rarely happens), you have time between features (almost never), you\u0026rsquo;re not fighting production fires (frequently untrue), and nobody\u0026rsquo;s pressuring you to ship faster (never).\u003c/p\u003e\n\u003cp\u003eThe system ensures technical debt is always someone else\u0026rsquo;s problem. Always scheduled for later. Later never arrives.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-technical-debt-compounds-like-credit-card-interest\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#why-technical-debt-compounds-like-credit-card-interest\" title=\"Why Technical Debt Compounds Like Credit Card Interest\"\u003eWhy Technical Debt Compounds Like Credit Card Interest\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYour 2026 codebase reflects every shortcut from 2025. Every \u0026quot;we\u0026rsquo;ll fix it later.\u0026quot; Every suppressed warning. Every test you didn\u0026rsquo;t write. Cause and effect.\u003c/p\u003e\n\u003cp\u003eMost teams think they\u0026rsquo;re trading speed for quality. That\u0026rsquo;s not even wrong—it\u0026rsquo;s nonsense disguised as pragmatism. You\u0026rsquo;re choosing whether to pay now or pay later with interest. Later always costs more.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-cost-nobody-tracks\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#the-cost-nobody-tracks\" title=\"The Cost Nobody Tracks\"\u003eThe Cost Nobody Tracks\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTechnical debt is a business decision. Treat it like one.\u003c/p\u003e\n\u003cp\u003eWriting code fast but wrong costs more than writing it right. The 3 AM production incident. The Friday afternoon rollback. The six hours debugging something that proper async patterns would have prevented.\u003c/p\u003e\n\u003cp\u003eThese costs don\u0026rsquo;t show up in sprint reports. They show up in exhausted developers, missed deadlines, and customer incidents.\u003c/p\u003e\n\u003cp\u003eFeatures that should take days take weeks because the codebase fights you. Every change risks breaking something unrelated. \u0026ldquo;Just be careful\u0026rdquo; doesn\u0026rsquo;t scale.\u003c/p\u003e\n\u003cp\u003eDevelopers quit. Not because of salary. Because maintaining incomprehensible code destroys your soul. That new hire still lost after six months? Your architecture is the problem.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-actually-works-from-15-years-of-watching-teams-fail\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-actually-works-from-15-years-of-watching-teams-fail\" title=\"What Actually Works (From 15 Years of Watching Teams Fail)\"\u003eWhat Actually Works (From 15 Years of Watching Teams Fail)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eSustainable doesn\u0026rsquo;t mean perfect. It means the codebase doesn\u0026rsquo;t actively fight you.\u003c/p\u003e\n\u003cp\u003eWrite tests because they save debugging time, not because some \u0026ldquo;best practices\u0026rdquo; document says to. I\u0026rsquo;ve watched developers spend three days tracking down a bug that a 15-line unit test would have caught in three seconds. That\u0026rsquo;s not a best practice—that\u0026rsquo;s basic economics.\u003c/p\u003e\n\u003cp\u003eRefactor as you go. Not in some mythical future sprint. When you\u0026rsquo;re in a file and you see garbage code, fix it then. Yes, even if it\u0026rsquo;s \u0026ldquo;out of scope.\u0026rdquo; Especially if it\u0026rsquo;s out of scope. The Boy Scout Rule isn\u0026rsquo;t a suggestion—it\u0026rsquo;s how you avoid code rot.\u003c/p\u003e\n\u003cp\u003ePush back on scope creep with data. \u0026ldquo;This will take three days with tests, one day without\u0026rdquo; is a lie everyone tells. It takes three days either way—you\u0026rsquo;re just choosing whether to spend them now or during the 2 AM production incident.\u003c/p\u003e\n\u003cp\u003eYour .NET project in 2026 has every tool needed to avoid this. Roslyn analyzers like \u003ca href=\"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1062\u003c/a\u003e catch null reference exceptions before they ship. \u003ca href=\"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA2007\u003c/a\u003e prevents ConfigureAwait deadlocks automatically. In my MCT courses, I enable these analyzers on legacy projects. Within hours, they\u0026rsquo;ve found bugs that lived in production for years.\u003c/p\u003e\n\u003cp\u003eNUnit\u0026rsquo;s parameterized tests let you cover edge cases in three lines. C# 12\u0026rsquo;s primary constructors eliminate the boilerplate nobody ever tested. .NET 9\u0026rsquo;s performance improvements mean you can write cleaner code that\u0026rsquo;s also faster.\u003c/p\u003e\n\u003cp\u003eTools aren\u0026rsquo;t the problem. You can download Visual Studio 2022, enable all the analyzers, and catch 80% of common bugs before your first commit.\u003c/p\u003e\n\u003cp\u003eDiscipline is the problem.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-2026-actually-offers\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-2026-actually-offers\" title=\"What 2026 Actually Offers\"\u003eWhat 2026 Actually Offers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e2026 isn\u0026rsquo;t special. .NET 9 is mature and stable now—shipped November 2024. C# 13 brought some nice features. The ecosystem keeps improving.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s what matters: The tools to build maintainable software have been available for years. Roslyn analyzers. Testing frameworks. Structured logging. Observability tools. None of this is new.\u003c/p\u003e\n\u003cp\u003eThe bottleneck was never tooling. It\u0026rsquo;s discipline.\u003c/p\u003e\n\u003cp\u003eWhat makes 2026 different? Nothing, unless you decide it is.\u003c/p\u003e\n\u003cp\u003eYou can start the year like every other year—good intentions, abandoned by February. Or you can actually change something.\u003c/p\u003e\n\u003cp\u003eNot by adopting the newest framework. Not by rewriting everything in the latest architectural pattern. By making the unsexy choice: Fix one thing at a time. Add tests as you go. Enable analyzers. Refactor when you touch code, not \u0026ldquo;next quarter.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e.NET 9\u0026rsquo;s \u003ca href=\"https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eperformance improvements\u003c/a\u003e are real—LINQ is faster, JSON serialization allocates less, the JIT is smarter. Migrating from .NET 6 or 8 is straightforward. Most teams can do it in days.\u003c/p\u003e\n\u003cp\u003eC# 13\u0026rsquo;s params collections and field keyword are fine. Use them where they help. Ignore them where they don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eAzure\u0026rsquo;s container and serverless offerings are stable now. Pick what fits your team\u0026rsquo;s expertise. Ignore what Hacker News says is \u0026ldquo;modern.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eAI integration will be 2026\u0026rsquo;s buzzword. Most will be snake oil. Some—like GitHub Copilot for boilerplate—actually helps. Don\u0026rsquo;t chase hype. Solve real problems.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"code-that-doesnt-wake-you-up-at-3-am\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#code-that-doesnt-wake-you-up-at-3-am\" title=\"Code That Doesn\u0026rsquo;t Wake You Up at 3 AM\"\u003eCode That Doesn\u0026rsquo;t Wake You Up at 3 AM\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntentional development looks boring. No clever patterns. No abstraction for abstraction\u0026rsquo;s sake. Just code that works and can be debugged when it doesn\u0026rsquo;t.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.Extensions.Logging\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Diagnostics\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomerOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eActivitySource\u003c/span\u003e \u003cspan class=\"n\"\u003e_activitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIOrderRepository\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomerOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eActivitySource\u003c/span\u003e \u003cspan class=\"n\"\u003eactivitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eIOrderRepository\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_activitySource\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eactivitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// OpenTelemetry distributed tracing - you\u0026#39;ll thank me during the incident\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_activitySource\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartActivity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ProcessOrder\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetTag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;order.id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetTag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;customer.id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"s\"\u003e\u0026#34;Processing order {OrderId} for customer {CustomerId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// CA2007 compliant - no deadlocks in ASP.NET synchronization context\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureAwait\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsValid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// Structured logging means you can query this in Application Insights\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"s\"\u003e\u0026#34;Order validation failed for {OrderId}: {ValidationErrors}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJoin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;, \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrors\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eActivityStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Validation failed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValidationFailed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrors\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureAwait\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eactivity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetTag\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;order.total\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is production code, adapted from a real order-processing system. Nothing fancy. Dependency injection makes it testable—I can mock \u003ccode\u003eIOrderRepository\u003c/code\u003e in unit tests. Structured logging means when something breaks at 3 AM, I can find it in Azure Application Insights in thirty seconds instead of thirty minutes. OpenTelemetry gives me distributed traces across services. ConfigureAwait prevents the deadlocks that plagued the previous version.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s not clever. It\u0026rsquo;s reliable. After fifteen years, I\u0026rsquo;ll take reliable over clever every single time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-2026-different\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#making-2026-different\" title=\"Making 2026 Different\"\u003eMaking 2026 Different\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLearning new C# features isn\u0026rsquo;t hard. Optimizing Azure costs isn\u0026rsquo;t hard. Discipline is hard.\u003c/p\u003e\n\u003cp\u003eSaying no when a VP wants a feature that solves no real problem. Budgeting time for maintenance and defending it. Writing tests when nobody\u0026rsquo;s watching. Code reviewing properly when you\u0026rsquo;re swamped. Having uncomfortable conversations about quality.\u003c/p\u003e\n\u003cp\u003eMeasuring what matters: incident rates, recovery time, PR review duration, developer retention. Not just velocity.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-january-looks-like-for-most-teams\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-january-looks-like-for-most-teams\" title=\"What January Looks Like for Most Teams\"\u003eWhat January Looks Like for Most Teams\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eJanuary: \u0026ldquo;This year will be different.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eFebruary: Feature roadmap consumed the quarter.\u003c/p\u003e\n\u003cp\u003eMarch: Production incident. All hands on deck.\u003c/p\u003e\n\u003cp\u003eApril-December: Repeat.\u003c/p\u003e\n\u003cp\u003eDiscipline is hard because it requires saying no to immediate pressure for long-term stability. The pressure is real. The meetings asking \u0026ldquo;why so long\u0026rdquo; are real. The 5 PM Friday Slack messages are real.\u003c/p\u003e\n\u003cp\u003eTeams that survive aren\u0026rsquo;t smarter. They\u0026rsquo;re not using better frameworks. They have organizational support for saying no. Or they\u0026rsquo;re stubborn enough to do it anyway.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-winning-in-2026-looks-like\"\u003e\u003ca href=\"/posts/happy-new-year-2026/#what-winning-in-2026-looks-like\" title=\"What Winning in 2026 Looks Like\"\u003eWhat Winning in 2026 Looks Like\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eShip fewer features that work, instead of many features that half work.\u003c/p\u003e\n\u003cp\u003eIncident rates go down. Developers stay. You can respond to market changes because you\u0026rsquo;re not buried in debt.\u003c/p\u003e\n\u003cp\u003eNot exciting. Pragmatic.\u003c/p\u003e\n\u003cp\u003eSo here\u0026rsquo;s my actual New Year wish for you: Stop lying to yourself about \u0026ldquo;next quarter.\u0026rdquo; Fix one thing this week. Enable one analyzer. Write one test. Refactor one function.\u003c/p\u003e\n\u003cp\u003eNot next quarter. This week.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s how software survives to 2027.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-01T14:00:00+01:00","id":"https://daily-devops.net/posts/happy-new-year-2026/","language":"en","summary":"Stop promising to fix technical debt next quarter. .NET 10, analyzers, and tests are ready in 2026; only the engineering discipline is missing.","tags":["technicaldebt","dotnet","csharp","softwareengineering","codequality"],"title":"Most Software Teams Are Lying to Themselves—2026 Needs to Be Different","url":"https://daily-devops.net/posts/happy-new-year-2026/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLet\u0026rsquo;s be honest about 2025: no runtime breakthroughs, no language revolutions. Nothing that\u0026rsquo;ll make the keynote highlight reels. What we got instead was something the ecosystem desperately needed—tooling that finally stopped lying about complexity.\u003c/p\u003e\n\u003cp\u003eThe wins came from admitting reality. Distributed systems aren\u0026rsquo;t simple, and tools that pretend otherwise just create delayed failures. Async execution semantics matter, whether your abstraction acknowledges them or not. Infrastructure dependencies aren\u0026rsquo;t implementation details you can mock away without consequences. In 2025, the tools that delivered value made all of this explicit, testable, impossible to ignore.\u003c/p\u003e\n\u003cp\u003eBut alongside that technical progress, we also saw the cracks widen. Open source sustainability, corporate consumption patterns, ecosystem trust—these structural tensions didn\u0026rsquo;t get resolved. If anything, they became harder to ignore. And they\u0026rsquo;re shaping our tooling choices just as much as any technical consideration.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what actually mattered this year.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-complexity-visible-not-optional\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#making-complexity-visible-not-optional\" title=\"Making Complexity Visible, Not Optional\"\u003eMaking Complexity Visible, Not Optional\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern I kept seeing in 2025: tools that actually mattered forced you to deal with reality instead of pretending it away. Topology. Concurrency. Dependency lifecycles. Infrastructure behavior. The messy stuff we\u0026rsquo;ve been hiding behind \u0026ldquo;convenience\u0026rdquo; layers for years, just postponing production incidents.\u003c/p\u003e\n\u003cp\u003eAspire, TUnit, Testcontainers. Three different problems. One consistent theme: show me what\u0026rsquo;s actually happening.\u003c/p\u003e\n\u003cp\u003e.NET Aspire: Beyond the Azure Narrative\u003c/p\u003e\n\u003cp\u003eMost people look at Aspire and see Azure tooling. That\u0026rsquo;s reading it wrong. It\u0026rsquo;s worth correcting because it misses what actually changed in 2025.\u003c/p\u003e\n\u003cp\u003eI watched teams use Aspire in ways that had nothing to do with Azure. Polyglot systems where only the orchestration layer was .NET. Existing containerized services that got wired in without rewrites. Self-hosted infrastructure, alternative cloud providers, Docker on a developer\u0026rsquo;s laptop. Hybrid setups where Aspire was just the coordination layer, not the runtime.\u003c/p\u003e\n\u003cp\u003eWhat makes this work is that Aspire isn\u0026rsquo;t really about deployment targets. It\u0026rsquo;s about making system intent explicit.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDistributedApplication\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epostgres\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddPostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;db\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapi\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddProject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProjects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eApi\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;api\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                 \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithReference\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eLook at this code. Dependencies aren\u0026rsquo;t buried in appsettings files or injected through environment variables scattered across deployment scripts. They\u0026rsquo;re right there, versioned with your application code, reviewable in pull requests, enforced at composition time.\u003c/p\u003e\n\u003cp\u003eThe app model is your system topology as code. Aspire then \u0026ldquo;lowers\u0026rdquo; that high-level description into whatever you actually need—Kubernetes manifests, Bicep templates, Docker Compose files, whatever your target environment requires.\u003c/p\u003e\n\u003cp\u003eBut the thing that actually shifted conversations: observability gets baked in. With Aspire, OpenTelemetry isn\u0026rsquo;t a post-deployment retrofit. \u003ccode\u003eOTEL_SERVICE_NAME\u003c/code\u003e and \u003ccode\u003eOTEL_EXPORTER_OTLP_ENDPOINT\u003c/code\u003e are automatic. The dashboard shows you traces, logs, metrics during local dev—without the boilerplate.\u003c/p\u003e\n\u003cp\u003eWhen observability is structural instead of bolted-on, the entire conversation changes.\u003c/p\u003e\n\u003cp\u003eThat alignment—between how you describe your system, how it gets deployed, and how you observe it—is where Aspire delivered real value in 2025.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResources\u003c/strong\u003e: \u003ca href=\"https://github.com/dotnet/aspire\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub\u003c/a\u003e | \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/aspire/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eDocs\u003c/a\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"tunit-when-test-frameworks-hide-what-matters\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#tunit-when-test-frameworks-hide-what-matters\" title=\"TUnit: When Test Frameworks Hide What Matters\"\u003eTUnit: When Test Frameworks Hide What Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit looks like cleaner syntax. It\u0026rsquo;s not. The actual value is in execution semantics that most frameworks just ignore because they don\u0026rsquo;t care about precision.\u003c/p\u003e\n\u003cp\u003eReal test suites fail constantly for reasons that have nothing to do with your code. Shared state between parameterized tests. Async forced into sync silently. Parallel runs creating race conditions that only show up in CI. Test fixtures hiding execution boundaries you never designed for. The list goes on.\u003c/p\u003e\n\u003cp\u003eMost frameworks allow tests with these problems. TUnit makes them hard to accidentally create.\u003c/p\u003e\n\u003cp\u003eTake a realistic scenario—testing behavior that depends on multiple runtime dimensions like feature flags and tenant configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003esealed\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eFeatureFlagTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRequest_is_processed_correctly\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [Values(true, false)]\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003efeatureEnabled\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [Values(\u0026#34;Free\u0026#34;, \u0026#34;Premium\u0026#34;)]\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003etenantType\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esystem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eTestSystem\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efeatureEnabled\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etenantType\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresponse\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003esystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteRequestAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsSuccessful\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsTrue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn TUnit, each parameter combination runs in complete isolation. The async lifecycle is native—no hidden \u003ccode\u003eTask.Run()\u003c/code\u003e or \u003ccode\u003e.Result\u003c/code\u003e calls. Fixtures are explicit. Parallel execution doesn\u0026rsquo;t introduce coupling you didn\u0026rsquo;t ask for.\u003c/p\u003e\n\u003cp\u003eWhat this eliminates is that whole category of tests that pass locally, fail in CI, pass again when you re-run them, and fail on Tuesdays. You know the ones. The flaky tests that eat hours of investigation time because the failure mode has nothing to do with the business logic you\u0026rsquo;re testing.\u003c/p\u003e\n\u003cp\u003eIn production CI pipelines, I saw this translate to predictable parallel execution times, reduced variance across agents, and—most importantly—test failures that actually correlated with system behavior rather than execution artifacts.\u003c/p\u003e\n\u003cp\u003eTUnit makes execution boundaries explicit. That\u0026rsquo;s the real contribution.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResources\u003c/strong\u003e: \u003ca href=\"https://github.com/thomhurst/TUnit\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"testcontainers-when-mocks-stop-being-enough\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#testcontainers-when-mocks-stop-being-enough\" title=\"Testcontainers: When Mocks Stop Being Enough\"\u003eTestcontainers: When Mocks Stop Being Enough\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBy 2025, I stopped treating Testcontainers as optional. If you\u0026rsquo;re testing assumptions instead of real infrastructure, you\u0026rsquo;re setting yourself up for surprises in production.\u003c/p\u003e\n\u003cp\u003eIn-memory substitutes lie. You can\u0026rsquo;t test transaction isolation with SQLite. You can\u0026rsquo;t test Kafka\u0026rsquo;s partition rebalancing without Kafka. Message delivery semantics, startup timing, schema migrations—the real database handles all this differently than a polite fake.\u003c/p\u003e\n\u003cp\u003eTestcontainers lets you test actual infrastructure behavior:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ekafka\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eKafkaBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCleanUp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ekafka\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStartAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen these tests fail, they\u0026rsquo;re usually telling you about real production risks, not artifacts of your test harness.\u003c/p\u003e\n\u003cp\u003eConsider what this means for database testing. PostgreSQL handles concurrent transactions, deadlocks, constraint violations in ways that in-memory databases simply don\u0026rsquo;t. Kafka\u0026rsquo;s exactly-once semantics, partition assignment, consumer group rebalancing—you need the actual broker to test any of this meaningfully.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched too many teams ship code that works fine against mocks and breaks immediately in production. Connection pool exhaustion. Deadlocks under load. Message ordering violations during partition reassignment. Schema migrations that work on SQLite but fail on Postgres because of type handling differences.\u003c/p\u003e\n\u003cp\u003eThese aren\u0026rsquo;t edge cases. They\u0026rsquo;re the default in real systems.\u003c/p\u003e\n\u003cp\u003eTestcontainers spins up real containers in your CI pipeline. Tests run against actual systems. Then the containers get cleaned up. The feedback loop stays fast. The confidence isn\u0026rsquo;t false.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eResources\u003c/strong\u003e: \u003ca href=\"https://github.com/testcontainers/testcontainers-dotnet\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-structural-problems-were-not-solving\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#the-structural-problems-were-not-solving\" title=\"The Structural Problems We\u0026rsquo;re Not Solving\"\u003eThe Structural Problems We\u0026rsquo;re Not Solving\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe tooling highlights tell one story. But 2025 also made it harder to ignore structural problems that aren\u0026rsquo;t getting better.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"licensing-as-operational-dependency\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#licensing-as-operational-dependency\" title=\"Licensing as Operational Dependency\"\u003eLicensing as Operational Dependency\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCommercializing open source dependencies isn\u0026rsquo;t new. What became clearer in 2025 were the operational costs that don\u0026rsquo;t appear in pricing discussions.\u003c/p\u003e\n\u003cp\u003eCI pipelines started failing during container builds because license checks couldn\u0026rsquo;t reach licensing servers. Dependency upgrades got blocked not for technical reasons but because legal teams needed weeks to review new license terms. Build systems became coupled to licensing infrastructure in ways nobody had planned for. Features fragmented across paid and unpaid tiers, forcing architectural decisions based on licensing rather than technical fit.\u003c/p\u003e\n\u003cp\u003eFrom an RCDA perspective, this is a risk profile change. When your build breaks because a license server is down, you\u0026rsquo;ve introduced a runtime dependency that wasn\u0026rsquo;t part of the original technical evaluation. The feedback cycle slows. Operational complexity increases. And most teams don\u0026rsquo;t see this coming until they\u0026rsquo;re already committed to the dependency.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-consumption-contribution-imbalance\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#the-consumption-contribution-imbalance\" title=\"The Consumption-Contribution Imbalance\"\u003eThe Consumption-Contribution Imbalance\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLarge organizations continued extracting value from open source while contributing little back. Internal forks maintained indefinitely. Bug fixes applied internally but never pushed upstream. Copyright violations discovered through community audits, not voluntary disclosure.\u003c/p\u003e\n\u003cp\u003eIs this malicious? Usually not. It\u0026rsquo;s legal risk management, procurement friction, organizational complexity. But the outcome remains the same: ecosystem fragmentation and maintainer burnout, while enterprises save millions on software they couldn\u0026rsquo;t build themselves.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t sustainable. When consumption at scale doesn\u0026rsquo;t come with proportional contribution—whether that\u0026rsquo;s code, funding, security disclosures, or just documentation improvements—the ecosystem becomes extractive. Maintainers burn out. Critical libraries go unmaintained. Trust erodes.\u003c/p\u003e\n\u003cp\u003e2025 made this tension more visible. We still don\u0026rsquo;t have good answers.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-2025-actually-taught-us\"\u003e\u003ca href=\"/posts/dotnet-2025-year-in-review/#what-2025-actually-taught-us\" title=\"What 2025 Actually Taught Us\"\u003eWhat 2025 Actually Taught Us\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e2025 was the year .NET tooling stopped hiding what\u0026rsquo;s actually hard. Aspire made system intent explicit. TUnit made execution boundaries explicit. Testcontainers made infrastructure behavior explicit.\u003c/p\u003e\n\u003cp\u003eThe open source sustainability crisis? Still unresolved. Still worsening. And still being treated as someone else\u0026rsquo;s problem by many organizations extracting the most value. These aren\u0026rsquo;t abstract concerns—they shape which tools survive, which maintainers continue, which dependencies remain viable long-term.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the lesson: technical maturity and ecosystem health aren\u0026rsquo;t separate. Ignore sustainability problems and you eventually constrain technical progress. Build on foundations maintained by exhausted volunteers subsidizing enterprise infrastructure, and you\u0026rsquo;re building on uncertain ground.\u003c/p\u003e\n\u003cp\u003eThe tools that mattered were honest. They didn\u0026rsquo;t promise to make distributed systems simple. They didn\u0026rsquo;t pretend async execution doesn\u0026rsquo;t matter. They didn\u0026rsquo;t hide infrastructure behavior and hope you wouldn\u0026rsquo;t notice.\u003c/p\u003e\n\u003cp\u003eA mature ecosystem doesn\u0026rsquo;t have magic. It has tools that show you what\u0026rsquo;s happening so you can make real decisions instead of discovering the truth during an incident.\u003c/p\u003e\n\u003cp\u003eThe frameworks and libraries that\u0026rsquo;ll thrive going forward are the ones making system behavior transparent, testable, debuggable. Not the ones selling simplicity through opacity.\u003c/p\u003e\n\u003cp\u003e2025 taught us that honesty scales better than convenient abstractions that break under production load.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-30T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-2025-year-in-review/","language":"en","summary":"No runtime revolutions—Aspire, TUnit, and Testcontainers won by making distributed systems visible. Plus .NET's open source sustainability crisis.","tags":["opensource","architecture","dotnet","csharp","aspire","testing","softwareengineering","technicaldebt"],"title":"2025 in Review: The Year .NET Stopped Lying to Itself","url":"https://daily-devops.net/posts/dotnet-2025-year-in-review/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eI\u0026rsquo;ve watched developers, including myself, waste hundreds of hours on something completely avoidable. Not architecture decisions or complex algorithms—just typing. Specifically, typing the same \u003ccode\u003e.NET CLI\u003c/code\u003e commands over and over because they couldn\u0026rsquo;t quite remember the exact syntax.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;ve probably done it yourself. You\u0026rsquo;re about to add a NuGet package, you type \u003ccode\u003edotnet add package\u003c/code\u003e, then you pause. Was it \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e or \u003ccode\u003eMicrosoft.Extensions.Logging.Abstractions\u003c/code\u003e? You open a browser tab, search NuGet.org, find the package, copy the name, switch back to your terminal, paste it. Fifteen seconds lost. Multiply that by dozens of commands daily.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s not even counting the times you mistype a command and have to run it again. Or when you forget which flags \u003ccode\u003edotnet publish\u003c/code\u003e supports and end up in \u003ccode\u003e--help\u003c/code\u003e documentation.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003e.NET CLI\u003c/code\u003e has technically supported tab completion for years. But getting it to work? That meant diving into PowerShell documentation, copying \u003ccode\u003eRegister-ArgumentCompleter\u003c/code\u003e snippets from Stack Overflow, debugging why it wouldn\u0026rsquo;t load properly, then maintaining that brittle setup across machines. I tried it once on a project in 2021. Gave up after the third machine where it broke differently.\u003c/p\u003e\n\u003cp\u003eWhen \u003cstrong\u003e.NET 10\u003c/strong\u003e shipped in November 2025,  Microsoft finally included what should\u0026rsquo;ve been there from day one: native completion scripts. One command. That\u0026rsquo;s it. No registration. No manual shell configuration. Just \u003ccode\u003edotnet completions script \u0026gt;\u0026gt; $PROFILE\u003c/code\u003e and you\u0026rsquo;re done.\u003c/p\u003e\n\u003cp\u003eI tested it the day the release dropped. Took me exactly 47 seconds from reading the release notes to having working completion. That\u0026rsquo;s the kind of feature that makes you wonder why you tolerated the old way for so long.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-tab-completion-matters-more-than-you-think\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#why-tab-completion-matters-more-than-you-think\" title=\"Why Tab Completion Matters (More Than You Think)\"\u003eWhy Tab Completion Matters (More Than You Think)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s something I tracked for a week in October: I ran 847 \u003ccode\u003edotnet\u003c/code\u003e commands. That\u0026rsquo;s not an exceptional week—I was doing standard development work on four different projects. No CI/CD pipelines, no deployment scripts, just regular coding.\u003c/p\u003e\n\u003cp\u003eOf those 847 commands, 312 involved package names or project references. Before I enabled completion, I\u0026rsquo;d estimate I spent 10-15 seconds per command hunting for exact names. With completion? Two seconds. Tab, confirm, done.\u003c/p\u003e\n\u003cp\u003eDo the math on that. Even at a conservative 10 seconds saved per operation, that\u0026rsquo;s 3,120 seconds weekly. That\u0026rsquo;s 52 minutes I\u0026rsquo;m not spending on mechanical busywork. But here\u0026rsquo;s what really matters: the cognitive load disappears.\u003c/p\u003e\n\u003cp\u003eYou stop maintaining these useless mental indexes of syntax. I used to have \u003ccode\u003e-c\u003c/code\u003e vs \u003ccode\u003e--configuration\u003c/code\u003e memorized, along with whether it was \u003ccode\u003e--framework\u003c/code\u003e or \u003ccode\u003e-f\u003c/code\u003e, whether \u003ccode\u003epublish\u003c/code\u003e took \u003ccode\u003e--output\u003c/code\u003e or \u003ccode\u003e-o\u003c/code\u003e. Now? I type \u003ccode\u003edotnet publish -\u003c/code\u003e and press Tab. The shell shows me everything available. I pick what I need.\u003c/p\u003e\n\u003cp\u003eLast month I discovered \u003ccode\u003edotnet workload repair\u003c/code\u003e because it appeared in completion results. I\u0026rsquo;d been manually reinstalling workloads when they broke. Turns out there\u0026rsquo;s been a repair command since .NET 6. I just never knew because I never ran \u003ccode\u003edotnet --help\u003c/code\u003e looking for it.\u003c/p\u003e\n\u003cp\u003eThe modern \u003ccode\u003e.NET CLI\u003c/code\u003e does a lot more than most developers realize:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIt queries NuGet.org in real-time as you type package names. Type \u003ccode\u003edotnet add package Micro\u003c/code\u003e and hit Tab—you\u0026rsquo;ll see \u003ccode\u003eMicrosoft.Extensions.*\u003c/code\u003e packages before you finish typing.\u003c/li\u003e\n\u003cli\u003eIt understands your solution structure. In a multi-project solution, \u003ccode\u003edotnet add reference\u003c/code\u003e will show you the actual project files available, not force you to remember paths.\u003c/li\u003e\n\u003cli\u003eNested commands like \u003ccode\u003edotnet tool install\u003c/code\u003e have their own completion contexts. The shell knows when you\u0026rsquo;re specifying a tool name vs. a version vs. a configuration flag.\u003c/li\u003e\n\u003cli\u003eSome completions are context-aware. If you\u0026rsquo;ve already specified \u003ccode\u003e--framework net8.0\u003c/code\u003e, subsequent completions adjust accordingly.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe old approach—before \u003cstrong\u003e.NET 10\u003c/strong\u003e—worked but had a fundamental performance problem. Every tab press spawned a subprocess running \u003ccode\u003edotnet complete\u003c/code\u003e. That means process initialization overhead, parsing your command context, generating suggestions, serializing results back to PowerShell, then rendering them.\u003c/p\u003e\n\u003cp\u003eI measured this once on a moderately powerful dev machine (Ryzen 7, NVMe SSD, 32GB RAM). Simple completions like \u003ccode\u003edotnet b[Tab]\u003c/code\u003e took 80-120ms. Not terrible, but noticeable. Package completions that needed to query NuGet.org? 400-800ms depending on network latency.\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003e.NET 10\u003c/strong\u003e approach is architecturally different. The completion script that gets written to your \u003ccode\u003e$PROFILE\u003c/code\u003e contains the entire static grammar—every command, subcommand, and standard flag—compiled into shell-native code. Your shell (PowerShell, Bash, Zsh) can evaluate that code instantly because there\u0026rsquo;s no external process. It only invokes \u003ccode\u003edotnet complete\u003c/code\u003e when you hit something dynamic like package names or project file paths. The difference is immediately perceptible. Completions feel instant because most of them actually are instant.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-evolution-from-dynamic-to-native-completion\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-evolution-from-dynamic-to-native-completion\" title=\"The Evolution: From Dynamic to Native Completion\"\u003eThe Evolution: From Dynamic to Native Completion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUnderstanding what changed in \u003cstrong\u003e.NET 10\u003c/strong\u003e gives context to why this matters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-old-way-dynamic-completion-pre-net-10\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-old-way-dynamic-completion-pre-net-10\" title=\"The Old Way: Dynamic Completion (Pre-.NET 10)\"\u003eThe Old Way: Dynamic Completion (Pre-.NET 10)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor \u003ccode\u003e.NET\u003c/code\u003e versions before 10, if you wanted tab completion, you needed to register an argument completer in your PowerShell profile. The approach was straightforward but had overhead:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# The legacy approach that still works\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eRegister-ArgumentCompleter\u003c/span\u003e \u003cspan class=\"n\"\u003e-Native\u003c/span\u003e \u003cspan class=\"n\"\u003e-CommandName\u003c/span\u003e \u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003e-ScriptBlock\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eparam\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$wordToComplete\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$commandAst\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cursorPosition\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecomplete\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003e-position\u003c/span\u003e \u003cspan class=\"nv\"\u003e$cursorPosition\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"nv\"\u003e$commandAst\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"nb\"\u003eForEach-Object\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eSystem.Management.Automation.CompletionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e]::\u003c/span\u003e\u003cspan class=\"n\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$_\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;ParameterValue\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$_\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis approach technically worked. But every Tab press meant PowerShell had to invoke \u003ccode\u003edotnet complete\u003c/code\u003e as a subprocess, wait for it to parse your current command, generate completions, and return them. On my old laptop (circa 2019), this sometimes took long enough that I\u0026rsquo;d press Tab, see nothing happen, assume completion wasn\u0026rsquo;t working, and just finish typing manually.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-new-way-native-completions-net-10\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-new-way-native-completions-net-10\" title=\"The New Way: Native Completions (.NET 10\u0026#43;)\"\u003eThe New Way: Native Completions (.NET 10+)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEnter \u003cstrong\u003e.NET 10\u003c/strong\u003e. Microsoft introduced the \u003ccode\u003edotnet completions script\u003c/code\u003e command that generates shell-specific completion code. This code:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eHandles static grammar directly\u003c/strong\u003e in the shell without invoking \u003ccode\u003edotnet complete\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFalls back intelligently\u003c/strong\u003e only for dynamic content (like NuGet package names)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntegrates natively\u003c/strong\u003e with your shell\u0026rsquo;s completion system\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDelivers near-instant results\u003c/strong\u003e for common operations\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe result? Noticeably faster, smoother completion experience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-one-liner-that-changes-everything\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-one-liner-that-changes-everything\" title=\"The One-Liner That Changes Everything\"\u003eThe One-Liner That Changes Everything\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEverything we\u0026rsquo;ve discussed leads to this single, elegant line:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat\u0026rsquo;s genuinely all you need. One command, executed once, and you\u0026rsquo;re done. Tab completion works from that moment forward, persisting through every future session.\u003c/p\u003e\n\u003cp\u003eNow, while you could stop here and be perfectly fine, understanding what\u0026rsquo;s actually happening beneath the surface transforms this from \u0026ldquo;magic command\u0026rdquo; to something you can troubleshoot and maintain confidently. So let\u0026rsquo;s break down what\u0026rsquo;s really going on.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-this-command-does\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#what-this-command-does\" title=\"What This Command Does\"\u003eWhat This Command Does\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLet\u0026rsquo;s examine each component. First, the command itself:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis single command tells the \u003ccode\u003e.NET CLI\u003c/code\u003e to generate a completion script for your shell. The \u003ccode\u003edotnet\u003c/code\u003e executable is intelligent about this—it examines your environment, detects which shell you\u0026rsquo;re currently running, and outputs the appropriate completion code for that shell. On Windows systems, it defaults to PowerShell (\u003ccode\u003epwsh\u003c/code\u003e). On Linux or macOS, it checks your environment variables to determine whether you\u0026rsquo;re using Bash, Zsh, Fish, or Nushell, and generates the right script accordingly. This automatic detection removes another friction point—you don\u0026rsquo;t have to tell it which shell you want.\u003c/p\u003e\n\u003cp\u003eThe second part of the equation is the redirection operator:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe double angle-bracket (\u003ccode\u003e\u0026gt;\u0026gt;\u003c/code\u003e) is PowerShell\u0026rsquo;s append operator. It takes everything the \u003ccode\u003edotnet completions script\u003c/code\u003e command outputs and appends it to your PowerShell profile file. The \u003ccode\u003e$PROFILE\u003c/code\u003e is an automatic variable that PowerShell sets during startup—it points to your current user\u0026rsquo;s current host profile. For most Windows developers, this lives at:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e$HOME\\Documents\\PowerShell\\Microsoft.PowerShell_profile.ps1\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eBut the beauty of using \u003ccode\u003e$PROFILE\u003c/code\u003e is that you don\u0026rsquo;t need to know or remember the exact path. PowerShell handles it for you.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-approach-is-brilliant\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#why-this-approach-is-brilliant\" title=\"Why This Approach Is Brilliant\"\u003eWhy This Approach Is Brilliant\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis one-liner is a masterclass in pragmatic design. It leverages several elegant PowerShell concepts that make it simultaneously powerful and forgiving:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAutomatic environment detection\u003c/strong\u003e means you don\u0026rsquo;t need to tell the \u003ccode\u003edotnet\u003c/code\u003e executable anything about your shell. It figures it out. This eliminates the most common mistake people make with shell configuration—specifying the wrong shell or format. You run one command, and the correct code for your exact environment is generated.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eProfile persistence\u003c/strong\u003e ensures your setup survives across sessions. Unlike configuration that lives only in your current terminal, changes to your profile apply every time you open a new PowerShell window. This is how you move from \u0026ldquo;temporary configuration\u0026rdquo; to \u0026ldquo;permanent improvement.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSafe appending\u003c/strong\u003e with the \u003ccode\u003e\u0026gt;\u0026gt;\u003c/code\u003e operator is crucial. This isn\u0026rsquo;t destructive. You\u0026rsquo;re not overwriting your profile—you\u0026rsquo;re adding to it. If you\u0026rsquo;ve already customized your profile with functions, aliases, or other settings, they all remain untouched. The completion script just appends at the end. This means you can run the command multiple times without fear. It\u0026rsquo;s idempotent.\u003c/p\u003e\n\u003cp\u003eThis combination of automatic detection, persistence, and safety is pragmatic design at its best. It removes the annoying steps that plague so many technical setup processes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"making-it-stick-the-powershell-profile-deep-dive\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#making-it-stick-the-powershell-profile-deep-dive\" title=\"Making It Stick: The PowerShell Profile Deep Dive\"\u003eMaking It Stick: The PowerShell Profile Deep Dive\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is where the process becomes slightly more involved—but also where understanding matters. Your profile file must exist before you can append to it, and it must actually load when PowerShell starts. Both of these requirements are usually met, but not always. Let\u0026rsquo;s make sure you\u0026rsquo;re covered.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"verifying-your-profile-path\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#verifying-your-profile-path\" title=\"Verifying Your Profile Path\"\u003eVerifying Your Profile Path\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFirst, see where PowerShell thinks your profile lives:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"nb\"\u003eSelect-Object\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis will show you all available profile paths. The one labeled \u003ccode\u003eMicrosoft.PowerShell_profile.ps1\u003c/code\u003e in your Documents folder is what we care about.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"creating-your-profile-if-it-doesnt-exist\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#creating-your-profile-if-it-doesnt-exist\" title=\"Creating Your Profile If It Doesn\u0026rsquo;t Exist\"\u003eCreating Your Profile If It Doesn\u0026rsquo;t Exist\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePowerShell doesn\u0026rsquo;t create your profile automatically. If you\u0026rsquo;ve never customized PowerShell before, you might not have one. Create it like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!(\u003c/span\u003e\u003cspan class=\"nb\"\u003eTest-Path\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eNew-Item\u003c/span\u003e \u003cspan class=\"n\"\u003e-ItemType\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"n\"\u003e-Force\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is defensive: if the profile doesn\u0026rsquo;t exist, create it. If it does, do nothing.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"actually-adding-tab-completion\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#actually-adding-tab-completion\" title=\"Actually Adding Tab Completion\"\u003eActually Adding Tab Completion\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNow that you know your profile exists, add the completion:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"activating-it-in-your-current-session\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#activating-it-in-your-current-session\" title=\"Activating It in Your Current Session\"\u003eActivating It in Your Current Session\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe profile runs automatically when you open a new PowerShell window. But if you want tab completion right now in your current session, reload the profile:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe dot (\u003ccode\u003e.\u003c/code\u003e) is PowerShell\u0026rsquo;s dot-sourcing operator. It executes the profile file in the current session\u0026rsquo;s scope, making all its contents immediately available.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"testing-your-setup\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#testing-your-setup\" title=\"Testing Your Setup\"\u003eTesting Your Setup\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAfter you\u0026rsquo;ve run the setup, test it immediately. Don\u0026rsquo;t trust that it worked—verify it. Open PowerShell and type:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eTab\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf it\u0026rsquo;s working, you\u0026rsquo;ll see \u003ccode\u003eadd\u003c/code\u003e appear instantly. Press Tab again and you\u0026rsquo;ll cycle through \u003ccode\u003eanalyze\u003c/code\u003e and any other \u0026lsquo;a\u0026rsquo; commands. That\u0026rsquo;s the static completion engine—your shell already knows those commands exist.\u003c/p\u003e\n\u003cp\u003eIf nothing happens, something\u0026rsquo;s wrong. Don\u0026rsquo;t waste time wondering why. Jump to the troubleshooting section below.\u003c/p\u003e\n\u003cp\u003eNow let\u0026rsquo;s test something more complex that exercises the dynamic side of completion:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003eadd\u003c/span\u003e \u003cspan class=\"n\"\u003epackage\u003c/span\u003e \u003cspan class=\"n\"\u003eMicro\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eTab\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eType this exactly and press Tab. Within a moment, you\u0026rsquo;ll see suggestions for NuGet packages starting with \u0026ldquo;Micro\u0026rdquo;—\u003ccode\u003eMicrosoft.AspNetCore.App\u003c/code\u003e, \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e, and dozens of others. This is where the hybrid approach really shines. Your shell instantly handles the known parts (\u003ccode\u003edotnet add package\u003c/code\u003e), then intelligently queries NuGet.org for available packages that match your prefix. You see results without any delay, yet they\u0026rsquo;re genuinely dynamic and current.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-completion-modes-why-hybrid-matters\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#understanding-completion-modes-why-hybrid-matters\" title=\"Understanding Completion Modes: Why Hybrid Matters\"\u003eUnderstanding Completion Modes: Why Hybrid Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s documentation distinguishes between two different completion strategies, and understanding this distinction helps you appreciate why \u003cstrong\u003e.NET 10\u003c/strong\u003e is such a significant upgrade.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"hybrid-completion-the-net-10-standard\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#hybrid-completion-the-net-10-standard\" title=\"Hybrid Completion: The .NET 10 Standard\"\u003eHybrid Completion: The \u003cstrong\u003e.NET 10\u003c/strong\u003e Standard\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHybrid completion is what you get when using the native completion scripts in \u003cstrong\u003e.NET 10\u003c/strong\u003e or later with PowerShell, Bash, or Zsh. The strategy is elegantly split:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eStatic grammar is handled directly\u003c/strong\u003e by shell code that was generated specifically for your shell. The shell already knows about all the \u003ccode\u003e.NET CLI\u003c/code\u003e commands, subcommands, and standard flags. This runs instantly, without any external process.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDynamic content\u003c/strong\u003e triggers \u003ccode\u003edotnet complete\u003c/code\u003e only when necessary. Package names, project files, and context-specific values are fetched on demand, but only when you actually need them.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis hybrid architecture is why completion feels so responsive. I compared it directly: on the same machine, the old \u003ccode\u003eRegister-ArgumentCompleter\u003c/code\u003e approach took 95ms average for static completions. Native completion? 8ms. That\u0026rsquo;s over 10x faster, and you feel it.\u003c/p\u003e\n\u003cp\u003eFor dynamic package completions, both approaches need to query NuGet, so they\u0026rsquo;re roughly equivalent (around 500ms depending on your network). But the difference is that 90% of your completions are static. Microsoft clearly spent time optimizing where it matters most.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"dynamic-completion-the-legacy-approach\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#dynamic-completion-the-legacy-approach\" title=\"Dynamic Completion: The Legacy Approach\"\u003eDynamic Completion: The Legacy Approach\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you\u0026rsquo;re running \u003ccode\u003e.NET 9\u003c/code\u003e or earlier, or if you\u0026rsquo;ve configured completion using the older registration method, every single completion request—even for static commands—invokes the \u003ccode\u003edotnet complete\u003c/code\u003e command in a subprocess. This approach works, absolutely. But it\u0026rsquo;s noticeably slower. You press Tab and wait for a process to start, execute, and return results. For simple completions, the wait is usually acceptable. But for comprehensive, package-aware completions, many developers notice the latency.\u003c/p\u003e\n\u003cp\u003eThis is why upgrading to \u003cstrong\u003e.NET 10\u003c/strong\u003e and enabling native completion is worth doing. You\u0026rsquo;re not just getting a feature—you\u0026rsquo;re getting a more responsive development experience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-should-you-enable-this\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#when-should-you-enable-this\" title=\"When Should You Enable This?\"\u003eWhen Should You Enable This?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe honest answer: immediately. There is genuinely no downside to enabling tab completion for the \u003ccode\u003e.NET CLI\u003c/code\u003e. It\u0026rsquo;s pure upside—faster work, fewer mistakes, better exploration of available commands—with zero risk and minimal setup.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;ll see particularly significant benefits if you:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCreate projects frequently\u003c/strong\u003e using \u003ccode\u003edotnet new\u003c/code\u003e. Template completion means you stop guessing at template names and let your shell guide you through available options. This is especially valuable when you need templates for specific purposes and can\u0026rsquo;t quite remember the exact name.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eManage NuGet dependencies regularly\u003c/strong\u003e with \u003ccode\u003edotnet add package\u003c/code\u003e. Package completion transforms this from \u0026ldquo;Hunt for the right package on NuGet.org, copy the name, paste it in the terminal\u0026rdquo; to \u0026ldquo;Type a prefix and tab through suggestions.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWork with multiple solutions and projects\u003c/strong\u003e where \u003ccode\u003edotnet publish\u003c/code\u003e, \u003ccode\u003edotnet pack\u003c/code\u003e, and similar commands need project file context. Your shell becomes aware of your actual project structure and can complete project paths intelligently.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUse complex build or publish profiles\u003c/strong\u003e where remembering the exact configuration names and publish targets becomes tedious. Let completion handle the recall.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWant to explore CLI capabilities\u003c/strong\u003e beyond the commands you use regularly. Completion surfaces available subcommands and options, making it easier to discover capabilities you didn\u0026rsquo;t know existed. How many developers skip learning some feature simply because they didn\u0026rsquo;t know it was available?\u003c/p\u003e\n\u003cp\u003eEven if you\u0026rsquo;re an IDE enthusiast who spends most time in Visual Studio rather than the terminal, tab completion removes a psychological barrier to shell adoption. Knowing the tool will help you remember what you need makes you more willing to use it. That\u0026rsquo;s a genuine quality-of-life improvement.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-things-dont-work-troubleshooting\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#when-things-dont-work-troubleshooting\" title=\"When Things Don\u0026rsquo;t Work: Troubleshooting\"\u003eWhen Things Don\u0026rsquo;t Work: Troubleshooting\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost of the time this works on the first try. But I\u0026rsquo;ve helped enough people set this up to know the failure modes. Here\u0026rsquo;s what actually breaks and how to fix it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"your-profile-exists-but-isnt-loading\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#your-profile-exists-but-isnt-loading\" title=\"Your Profile Exists But Isn\u0026rsquo;t Loading\"\u003eYour Profile Exists But Isn\u0026rsquo;t Loading\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe most common issue is that your profile file exists, but PowerShell isn\u0026rsquo;t executing it. This usually indicates an execution policy problem. Check what policy is currently set:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eGet-ExecutionPolicy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf this returns \u003ccode\u003eRestricted\u003c/code\u003e, PowerShell refuses to run any scripts, including your profile. This is the default on many systems. You need to change it. The recommended setting is \u003ccode\u003eRemoteSigned\u003c/code\u003e, which allows scripts you created locally to run while blocking scripts downloaded from the internet—a good security balance:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eSet-ExecutionPolicy\u003c/span\u003e \u003cspan class=\"n\"\u003e-ExecutionPolicy\u003c/span\u003e \u003cspan class=\"n\"\u003eRemoteSigned\u003c/span\u003e \u003cspan class=\"n\"\u003e-Scope\u003c/span\u003e \u003cspan class=\"n\"\u003eCurrentUser\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis sets the policy for your user account specifically, without requiring administrative elevation. Once you\u0026rsquo;ve run this, your profile will load and execute automatically.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"completion-still-isnt-working-after-setup\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#completion-still-isnt-working-after-setup\" title=\"Completion Still Isn\u0026rsquo;t Working After Setup\"\u003eCompletion Still Isn\u0026rsquo;t Working After Setup\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYour profile loaded, but tab completion still doesn\u0026rsquo;t appear when you try it. Walk through this checklist:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eVerify you have \u003cstrong\u003e.NET 10\u003c/strong\u003e or later installed:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003e-version\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNative completion scripts only exist in \u003ccode\u003e.NET 10+\u003c/code\u003e. If you\u0026rsquo;re on \u003ccode\u003e.NET 9\u003c/code\u003e or earlier, the command won\u0026rsquo;t generate anything.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eConfirm the completion script was actually appended to your profile:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eGet-Content\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"nb\"\u003eSelect-String\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;dotnet completions\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis checks whether the profile file contains the completion script. If it returns nothing, the append didn\u0026rsquo;t work. Check that your profile path is accessible and writable.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eReload your profile or open a fresh terminal:\u003c/strong\u003e\nIf the script is there but completion doesn\u0026rsquo;t work, your current session hasn\u0026rsquo;t loaded it yet. Run \u003ccode\u003e. $PROFILE\u003c/code\u003e to reload, or simply close and reopen PowerShell.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch3 id=\"completion-feels-slow\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#completion-feels-slow\" title=\"Completion Feels Slow\"\u003eCompletion Feels Slow\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you notice completion taking a second or two to respond, especially on complex queries, you\u0026rsquo;re likely experiencing the dynamic fallback in action. When you request NuGet package suggestions or other context-dependent completions, PowerShell invokes \u003ccode\u003edotnet complete\u003c/code\u003e in the background. This is expected behavior, not a bug. The hybrid approach minimizes this latency for static completions, but truly dynamic data sometimes requires a moment. For most users, the responsiveness improvement over pre-.NET 10 completion is still significant.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-clis-evolving-scope-context-matters\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-clis-evolving-scope-context-matters\" title=\"The CLI\u0026rsquo;s Evolving Scope: Context Matters\"\u003eThe CLI\u0026rsquo;s Evolving Scope: Context Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTab completion might seem like a small feature, but it\u0026rsquo;s actually a signal of something larger happening with the \u003ccode\u003e.NET CLI\u003c/code\u003e. The tool has evolved dramatically. Consider what you can accomplish from the command line today:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNative shell completions\u003c/strong\u003e now exist for PowerShell, Bash, Zsh, Fish, and Nushell—recognizing that .NET developers work across different operating systems and shells.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWorkload management\u003c/strong\u003e lets you install and manage platform-specific tools directly through the CLI—Swift for iOS development, NDK tooling, emulators.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGlobal tools and tool manifests\u003c/strong\u003e turn your development environment into a versioned, reproducible collection of utilities that travel with your projects.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSolution-level operations and dependency management\u003c/strong\u003e mean the CLI understands your entire solution structure, not just individual projects.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBuilt-in diagnostics and observability\u003c/strong\u003e help you understand what\u0026rsquo;s happening under the hood—from environment information to detailed build diagnostics.\u003c/p\u003e\n\u003cp\u003eThe journey from early \u003ccode\u003e.NET Core\u003c/code\u003e days to now is remarkable. The CLI started as a basic project builder. It\u0026rsquo;s now a sophisticated platform with growing capabilities. Tab completion, in this context, is Microsoft making a statement: we\u0026rsquo;ve built something complex and powerful, and we\u0026rsquo;re committed to making it accessible. You shouldn\u0026rsquo;t need to memorize obscure syntax or hunt through documentation for common operations. The tool should guide you. Completion is that commitment made practical.\u003c/p\u003e\n\u003cp\u003eFor developers who live at the command line, this kind of incremental thoughtfulness adds up. It\u0026rsquo;s not revolutionary. But it\u0026rsquo;s genuine progress.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-lazy-developers-summary\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#the-lazy-developers-summary\" title=\"The Lazy Developer\u0026rsquo;s Summary\"\u003eThe Lazy Developer\u0026rsquo;s Summary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWant the fastest path to productivity? Here\u0026rsquo;s the checklist:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eMake sure your PowerShell profile exists:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!(\u003c/span\u003e\u003cspan class=\"nb\"\u003eTest-Path\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eNew-Item\u003c/span\u003e \u003cspan class=\"n\"\u003e-ItemType\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e \u003cspan class=\"n\"\u003e-Path\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e \u003cspan class=\"n\"\u003e-Force\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eAdd the completion script:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ecompletions\u003c/span\u003e \u003cspan class=\"n\"\u003escript\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eReload in your current session:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e \u003cspan class=\"nv\"\u003e$PROFILE\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eVerify it works:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-pwsh\" data-lang=\"pwsh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edotnet\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"no\"\u003eTab\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe beauty of this approach is that it\u0026rsquo;s idempotent. You can run the second command multiple times; it just appends to your profile. It\u0026rsquo;s not elegant, but it\u0026rsquo;s practical.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts-the-compound-effect-of-small-improvements\"\u003e\u003ca href=\"/posts/dotnet-cli-expanding-scope-autocomplete/#final-thoughts-the-compound-effect-of-small-improvements\" title=\"Final Thoughts: The Compound Effect of Small Improvements\"\u003eFinal Thoughts: The Compound Effect of Small Improvements\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLook, I get it. Tab completion sounds trivial. \u0026ldquo;Just learn the commands\u0026rdquo; or \u0026ldquo;use the IDE\u0026rdquo; or whatever. I\u0026rsquo;ve heard all the dismissive responses.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s what actually happened after I enabled this: I stopped avoiding the CLI. Before, I\u0026rsquo;d often reach for Visual Studio\u0026rsquo;s NuGet manager or the solution explorer because I didn\u0026rsquo;t want to fight with command syntax. Now I stay in the terminal because it\u0026rsquo;s genuinely faster than switching contexts.\u003c/p\u003e\n\u003cp\u003eLast week I added twelve NuGet packages across four projects. Total time: maybe two minutes. Six months ago that would\u0026rsquo;ve been ten minutes minimum—switching to VS, waiting for NuGet to load, searching, selecting versions, clicking Install, waiting for restore.\u003c/p\u003e\n\u003cp\u003eThe time savings are real (I tracked 52 minutes weekly, remember), but the bigger win is staying in flow. Every context switch costs you focus. Every moment you spend hunting for syntax is a moment you\u0026rsquo;re not solving the actual problem.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003e.NET CLI\u003c/code\u003e has become a genuinely sophisticated tool—powerful enough to accomplish real work from the command line, yet complex enough that most developers never explore its full capabilities. Native tab completion in \u003cstrong\u003e.NET 10\u003c/strong\u003e is the accessibility layer that makes that power usable without constant cognitive overhead. It\u0026rsquo;s not flashy. It\u0026rsquo;s not revolutionary. But it\u0026rsquo;s the kind of thoughtful engineering that separates tools you tolerate from tools you actually \u003cem\u003eenjoy\u003c/em\u003e using.\u003c/p\u003e\n\u003cp\u003eThe setup takes ninety seconds. I timed it. Actually, I\u0026rsquo;ve timed it on six different machines now helping colleagues set this up. Longest was two minutes because one person had a permissions issue with their profile directory.\u003c/p\u003e\n\u003cp\u003eDo it now. Seriously—stop reading, open PowerShell, run the three commands in the summary below, test it with \u003ccode\u003edotnet a[Tab]\u003c/code\u003e. If it works, you just saved yourself dozens of hours over the next year. If it doesn\u0026rsquo;t work, the troubleshooting section will get you sorted.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve been using this daily since November 2024. It\u0026rsquo;s one of those rare features that actually lives up to the promise. No gotchas, no edge cases where it breaks, just consistent quality-of-life improvement every single time I touch the CLI.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-18T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-cli-expanding-scope-autocomplete/","language":"en","summary":".NET 10 ships native tab completion for the dotnet CLI. One command, no Register-ArgumentCompleter snippets, and your shell finally remembers.","tags":["cli","dotnet","bestpractices","devops","softwareengineering"],"title":"Stop Typing: The .NET CLI Tab Completion You've Been Missing","url":"https://daily-devops.net/posts/dotnet-cli-expanding-scope-autocomplete/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eSelecting a job scheduler is selecting an operational philosophy. The choice determines how your team thinks about background processing, what operational burdens you accept, and how your system scales as workloads grow. I\u0026rsquo;ve seen teams pick Quartz.NET for an MVP because \u003cem\u003e\u0026ldquo;we might need clustering eventually\u0026rdquo;,\u003c/em\u003e then spend three months fighting its complexity instead of shipping features.\u003c/p\u003e\n\u003cp\u003eA framework that simplifies development today might impose constraints tomorrow when throughput demands clustering or when job durability becomes non-negotiable. Conversely, adopting enterprise-grade features prematurely introduces complexity that slows iteration and increases onboarding friction.\u003c/p\u003e\n\u003cp\u003eThis article synthesizes the series into comparative analysis. It presents feature matrices, rates framework suitability across operational dimensions, and offers decision heuristics grounded in system maturity, infrastructure realities, and team capabilities. By the end, you\u0026rsquo;ll have a structured approach to selecting the scheduler that aligns with your needs—not the one with the most stars on GitHub.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"feature-matrix-what-each-framework-provides\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#feature-matrix-what-each-framework-provides\" title=\"Feature Matrix: What Each Framework Provides\"\u003eFeature Matrix: What Each Framework Provides\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe table below compares core capabilities across the five frameworks:\u003c/p\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eFeature\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eHangfire\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eQuartz.NET\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCoravel\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eNCronJob\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTickerQ\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSQL/Redis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSQL/Memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIn-memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIn-memory\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEF Core\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eClustering\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOptional\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDashboard\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes (SignalR)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAutomatic Retries\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCustom\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eManual\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eManual\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eCron Expressions\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eJob Calendars\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDependency Injection\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAsync-First\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartial\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eSource Generation\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eQueue Support\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eBatch Jobs\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePro only\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCustom\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eReal-Time Monitoring\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePolling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCustom\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLogs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLogs\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSignalR\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eExternal Dependencies\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNone\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMaturity (Years)\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e13+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e20+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e6+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2+\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2+\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThis matrix reveals trade-offs. Hangfire and Quartz.NET offer persistence and clustering but require databases. Coravel and NCronJob eliminate dependencies but sacrifice durability. TickerQ modernizes the stack with source generation and SignalR but lacks ecosystem maturity.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"suitability-ratings-across-dimensions\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#suitability-ratings-across-dimensions\" title=\"Suitability Ratings Across Dimensions\"\u003eSuitability Ratings Across Dimensions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe following ratings (1-5, where 5 is best) assess each framework across operational dimensions:\u003c/p\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDimension\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eHangfire\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eQuartz.NET\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCoravel\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eNCronJob\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eTickerQ\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eSimplicity\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eScalability\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eObservability\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDeveloper Experience\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eOperational Maturity\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eFlexibility\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity\u003c/strong\u003e: NCronJob and Coravel score highest—zero dependencies, minimal configuration. Quartz.NET scores lowest due to its steep learning curve.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e: Hangfire, Quartz.NET, and TickerQ provide database-backed durability. Coravel and NCronJob don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScalability\u003c/strong\u003e: Quartz.NET excels with robust clustering. Hangfire supports it but with limitations. Coravel and NCronJob don\u0026rsquo;t coordinate across instances.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eObservability\u003c/strong\u003e: Hangfire and TickerQ provide built-in dashboards. Quartz.NET requires custom listeners. Coravel and NCronJob rely on logging.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeveloper Experience\u003c/strong\u003e: Coravel and NCronJob prioritize fluent APIs and rapid integration. Quartz.NET\u0026rsquo;s complexity detracts from velocity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOperational Maturity\u003c/strong\u003e: Hangfire and Quartz.NET have extensive production validation. TickerQ and NCronJob are newer with smaller communities.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e: In-memory frameworks (NCronJob, Coravel) and TickerQ\u0026rsquo;s reflection-free design excel. Database-backed frameworks introduce latency.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFlexibility\u003c/strong\u003e: Quartz.NET\u0026rsquo;s advanced features (calendars, misfires) offer unmatched control. NCronJob\u0026rsquo;s minimalism limits customization.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"decision-heuristics-matching-framework-to-context\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#decision-heuristics-matching-framework-to-context\" title=\"Decision Heuristics: Matching Framework to Context\"\u003eDecision Heuristics: Matching Framework to Context\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSelecting a scheduler requires evaluating your system\u0026rsquo;s operational profile across several axes:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"system-maturity-and-workload-characteristics\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#system-maturity-and-workload-characteristics\" title=\"System Maturity and Workload Characteristics\"\u003eSystem Maturity and Workload Characteristics\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eEarly-stage startups or MVPs\u003c/strong\u003e: Prioritize speed. Use \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e to eliminate infrastructure overhead and accelerate feature delivery. Jobs are likely transient (cache warming, health checks), making persistence unnecessary. As the product matures, migrate to Hangfire or TickerQ if durability becomes critical.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGrowing applications with modest throughput\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e. Its persistence ensures reliability, dashboards provide visibility, and automatic retries reduce operational burden. It scales vertically (more workers per server) and horizontally (multiple servers with optional clustering) as workloads grow. Suitable for web applications processing hundreds to thousands of jobs per minute.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEnterprise systems with complex scheduling\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Its job calendars, misfire policies, and clustering support demanding workflows—financial batch processing, regulatory reporting, multi-tenant SaaS platforms. Operational complexity is justified by requirements Hangfire can\u0026rsquo;t meet: business day logic, priority-based execution, multi-datacenter coordination.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCloud-native microservices\u003c/strong\u003e: Use \u003cstrong\u003eNCronJob\u003c/strong\u003e. Its stateless design fits containerized deployments where ephemeral pods start, execute tasks, and terminate. Jobs should be idempotent to tolerate duplication across horizontal replicas. For critical workflows requiring persistence, use \u003cstrong\u003eTickerQ\u003c/strong\u003e integrated with your existing Entity Framework Core infrastructure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance-sensitive systems\u003c/strong\u003e: Use \u003cstrong\u003eTickerQ\u003c/strong\u003e. Source generation eliminates reflection overhead, async-first design maximizes throughput, and real-time monitoring via SignalR reduces operational latency. Ideal for SaaS platforms processing tens of thousands of jobs daily where every millisecond compounds across volume.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"infrastructure-constraints\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#infrastructure-constraints\" title=\"Infrastructure Constraints\"\u003eInfrastructure Constraints\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eNo database available\u003c/strong\u003e: Use \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e. Both run in-memory without external dependencies, fitting serverless functions, edge devices, or cost-constrained environments.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSQL Server or PostgreSQL in use\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e or \u003cstrong\u003eTickerQ\u003c/strong\u003e. Both integrate seamlessly with relational databases. Hangfire offers more storage backend options (MySQL, MongoDB); TickerQ requires Entity Framework Core.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRedis for caching\u003c/strong\u003e: Consider \u003cstrong\u003eHangfire with Redis storage\u003c/strong\u003e. It reduces database load and leverages existing infrastructure. Quartz.NET also supports Redis but requires more configuration.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eKubernetes or containerized deployments\u003c/strong\u003e: \u003cstrong\u003eNCronJob\u003c/strong\u003e fits naturally. For workflows requiring persistence, \u003cstrong\u003eTickerQ\u003c/strong\u003e works if you provision managed databases (Azure SQL, Amazon RDS). \u003cstrong\u003eHangfire\u003c/strong\u003e also fits but adds database management overhead.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"team-priorities-and-constraints\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#team-priorities-and-constraints\" title=\"Team Priorities and Constraints\"\u003eTeam Priorities and Constraints\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eDeveloper velocity is paramount\u003c/strong\u003e: Use \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e. Minimal configuration, fluent APIs, and zero operational overhead accelerate delivery. Ideal for small teams or solo developers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOperational reliability is critical\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e or \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Persistence, retries, and observability reduce risk of silent failures. Suitable for teams managing production systems where background jobs impact business operations.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eModern tooling and patterns preferred\u003c/strong\u003e: Use \u003cstrong\u003eTickerQ\u003c/strong\u003e. Source generation, SignalR, and Entity Framework Core integration appeal to teams comfortable with current .NET conventions. The learning curve is moderate but rewarding for performance-sensitive systems.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLegacy system maintenance\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e or \u003cstrong\u003eHangfire\u003c/strong\u003e. Both support .NET Framework, have extensive documentation, and integrate with older application architectures. TickerQ and NCronJob target modern .NET (6+).\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"scaling-and-operational-concerns\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#scaling-and-operational-concerns\" title=\"Scaling and Operational Concerns\"\u003eScaling and Operational Concerns\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eSingle instance, no scaling planned\u003c/strong\u003e: \u003cstrong\u003eCoravel\u003c/strong\u003e, \u003cstrong\u003eNCronJob\u003c/strong\u003e, or \u003cstrong\u003eHangfire\u003c/strong\u003e (without clustering) suffice. Persistence depends on job criticality—Hangfire if durability matters, Coravel/NCronJob if not.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHorizontal scaling with job coordination\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e (robust clustering) or \u003cstrong\u003eHangfire\u003c/strong\u003e (polling-based coordination). TickerQ supports clustering via Entity Framework Core optimistic concurrency but is less battle-tested at scale.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHigh throughput (tens of thousands of jobs/min)\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e with Redis or \u003cstrong\u003eTickerQ\u003c/strong\u003e. Hangfire\u0026rsquo;s polling introduces latency at extreme volumes. NCronJob and Coravel lack coordination mechanisms for distributed workloads.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMulti-region or geo-distributed\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Its clustering supports multiple datacenters with database replication. Hangfire can work but requires careful tuning. TickerQ\u0026rsquo;s youth makes it less proven in multi-region scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-selection-framework\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#practical-selection-framework\" title=\"Practical Selection Framework\"\u003ePractical Selection Framework\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUse this decision tree to narrow choices:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDo jobs need to survive application restarts?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Consider \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Proceed to step 2.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eWill you run multiple instances requiring coordination?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e (simple persistence, good observability).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Proceed to step 3.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDo you need advanced scheduling (calendars, misfires, priorities)?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Use \u003cstrong\u003eHangfire\u003c/strong\u003e (simpler than Quartz.NET, adequate clustering).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Use \u003cstrong\u003eQuartz.NET\u003c/strong\u003e (enterprise-grade features justify complexity).\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eIs performance (reflection-free, async-first) a top priority?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eYes, and you use Entity Framework Core\u003c/strong\u003e: Consider \u003cstrong\u003eTickerQ\u003c/strong\u003e (modern architecture, real-time monitoring).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYes, but no database\u003c/strong\u003e: Use \u003cstrong\u003eNCronJob\u003c/strong\u003e (minimal overhead, stateless).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Stick with \u003cstrong\u003eHangfire\u003c/strong\u003e or \u003cstrong\u003eQuartz.NET\u003c/strong\u003e based on feature needs.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDoes your team value developer velocity over advanced features?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eYes\u003c/strong\u003e: Use \u003cstrong\u003eCoravel\u003c/strong\u003e (fluent API, integrated queuing/caching/mailing).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo\u003c/strong\u003e: Select based on operational requirements (Hangfire for balance, Quartz.NET for control, TickerQ for modern tooling).\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"real-world-scenarios-and-recommendations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#real-world-scenarios-and-recommendations\" title=\"Real-World Scenarios and Recommendations\"\u003eReal-World Scenarios and Recommendations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 1: E-commerce platform processing order fulfillment workflows\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Persistence (orders must complete), retries (external APIs fail), observability (track order states).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: 10,000 orders/day, single application instance.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eHangfire\u003c/strong\u003e. Persistent storage ensures orders don\u0026rsquo;t vanish, automatic retries handle transient failures, dashboard provides real-time visibility. SQL Server likely already in use for order data.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 2: Internal metrics dashboard aggregating data every 10 minutes\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Simplicity, no persistence (restarting re-fetches data), single instance.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: 10 users, low stakes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e. Zero dependencies, fast integration. Coravel adds caching for metrics storage.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 3: Financial platform processing nightly batch reports\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Complex scheduling (business days, holidays), clustering (high availability), audit trails.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: Multi-datacenter, thousands of jobs.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eQuartz.NET\u003c/strong\u003e. Job calendars respect business rules, clustering ensures failover, listeners integrate with compliance auditing systems. Operational complexity justified by regulatory requirements.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 4: SaaS product with 50,000 users triggering reports on-demand\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Persistence, high throughput, real-time monitoring, modern architecture.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: Thousands of jobs/minute, horizontal scaling.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eTickerQ\u003c/strong\u003e if using Entity Framework Core, otherwise \u003cstrong\u003eHangfire with Redis\u003c/strong\u003e. TickerQ\u0026rsquo;s source generation and SignalR dashboard suit performance-sensitive SaaS. Hangfire\u0026rsquo;s broader ecosystem and maturity provide a safer fallback.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eScenario 5: Kubernetes-deployed microservices executing health checks\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNeeds\u003c/strong\u003e: Stateless, minimal overhead, idempotent tasks.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale\u003c/strong\u003e: Dozens of pod replicas, jobs tolerate duplication.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecommendation\u003c/strong\u003e: \u003cstrong\u003eNCronJob\u003c/strong\u003e. Direct \u003ccode\u003eIHostedService\u003c/code\u003e integration, zero dependencies, fits ephemeral containers perfectly.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"migration-paths-and-future-proofing\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#migration-paths-and-future-proofing\" title=\"Migration Paths and Future-Proofing\"\u003eMigration Paths and Future-Proofing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSystems evolve. A framework suitable today may become constraining tomorrow. Anticipate migration paths:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrom Coravel/NCronJob to Hangfire\u003c/strong\u003e: Straightforward. Replace in-memory scheduling with database-backed persistence. Job definitions remain similar—update registration code and add connection strings. No breaking application-level changes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrom Hangfire to Quartz.NET\u003c/strong\u003e: More involved. Hangfire\u0026rsquo;s simplicity (fire-and-forget, delayed, recurring) maps to Quartz.NET\u0026rsquo;s jobs and triggers, but Quartz.NET requires understanding its abstractions. Justify migration when Hangfire\u0026rsquo;s features prove insufficient (calendars, advanced misfires, multi-datacenter clustering).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFrom any framework to TickerQ\u003c/strong\u003e: Requires Entity Framework Core adoption and rewriting job definitions using attributes. Source generation introduces compile-time validation but necessitates build-time code changes. Worth the effort for teams prioritizing performance and modern patterns in greenfield projects or major refactors.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFuture-proofing tips\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAbstract job definitions\u003c/strong\u003e: Wrap scheduler-specific APIs in application-level abstractions. This reduces coupling and simplifies framework swaps.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLog extensively\u003c/strong\u003e: Regardless of scheduler, comprehensive logging enables observability when built-in tools lack.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonitor metrics\u003c/strong\u003e: Track job throughput, duration, failure rates. Export to Prometheus, Application Insights, or Datadog for centralized visibility.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDesign for idempotency\u003c/strong\u003e: Jobs that tolerate re-execution simplify failure recovery and enable horizontal scaling with minimal coordination.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"common-pitfalls-and-how-to-avoid-them\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#common-pitfalls-and-how-to-avoid-them\" title=\"Common Pitfalls and How to Avoid Them\"\u003eCommon Pitfalls and How to Avoid Them\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eChoosing based on features, not operational reality\u003c/strong\u003e: Quartz.NET\u0026rsquo;s advanced scheduling is impressive but overkill for applications running cron jobs daily. Match framework capabilities to actual requirements.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIgnoring infrastructure constraints\u003c/strong\u003e: Adopting Hangfire without provisioning databases delays deployment. Assess what infrastructure your organization supports before committing.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUnderestimating observability needs\u003c/strong\u003e: Logs suffice for small systems but become inadequate as job volumes grow. Dashboards (Hangfire, TickerQ) or custom telemetry (Quartz.NET with listeners) provide necessary visibility.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScaling prematurely\u003c/strong\u003e: Deploying Quartz.NET clustering for a single-instance application introduces complexity without benefit. Start simple (NCronJob, Coravel) and migrate when workload demands justify it.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNeglecting retry logic\u003c/strong\u003e: Frameworks without automatic retries (Coravel, NCronJob) require manual implementation. Don\u0026rsquo;t assume transient failures self-heal—code defensively.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-recommendations-by-use-case\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#final-recommendations-by-use-case\" title=\"Final Recommendations by Use Case\"\u003eFinal Recommendations by Use Case\u003c/a\u003e\u003c/h2\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eUse Case\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePrimary Choice\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAlternative\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAvoid\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMVP or early-stage product\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWeb application, moderate traffic\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eEnterprise with complex scheduling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire Pro\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMicroservices in Kubernetes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eHigh-performance SaaS platform\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire + Redis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInternal tools or low-stakes apps\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCoravel\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNCronJob\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eLegacy .NET Framework systems\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHangfire\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eQuartz.NET\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTickerQ, NCronJob\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\n\n\u003ch2 id=\"closing-thoughts\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/#closing-thoughts\" title=\"Closing Thoughts\"\u003eClosing Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJob scheduling is infrastructure that fades when chosen correctly and becomes friction when mismatched. The frameworks in this series span a spectrum from simplicity to control, each making deliberate trade-offs. Your choice should reflect your system\u0026rsquo;s current state and anticipated evolution—not aspirational architectures or feature envy.\u003c/p\u003e\n\u003cp\u003eStart with the simplest solution that meets your needs. Coravel and NCronJob eliminate overhead for transient workflows. Hangfire adds persistence and observability when reliability matters. Quartz.NET provides enterprise control when complexity is justified. TickerQ modernizes the stack with performance and real-time monitoring for cloud-native systems.\u003c/p\u003e\n\u003cp\u003eBackground processing done right becomes invisible enablers of system capability. Choose the scheduler that aligns with your operational philosophy, infrastructure constraints, and team priorities. The right framework disappears into the background, letting you focus on delivering business value rather than managing job execution mechanics.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-16T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-7-comparative-review/","language":"en","summary":"Side-by-side comparison of Hangfire, Quartz.NET, Coravel, NCronJob, and TickerQ with feature matrices and decision heuristics for .NET architects.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Choosing the Right Framework","url":"https://daily-devops.net/posts/dotnet-job-scheduling-7-comparative-review/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour SaaS platform processes tens of thousands of background jobs daily—user-triggered reports, scheduled data synchronization, recurring billing cycles. Performance matters: every millisecond spent in reflection overhead compounds across job volume. We measured it once: a 2ms reflection penalty per job execution meant an extra 40 seconds of CPU time daily across 20,000 jobs. Not catastrophic, but not free either.\u003c/p\u003e\n\u003cp\u003eObservability matters: product managers need real-time dashboards showing job states without deploying custom monitoring solutions. Safety matters: configuration errors should surface at compile time, not in production when a misspelled job name causes silent failures. We\u0026rsquo;ve all been there—typo in a cron expression, job never runs, customer discovers it three weeks later.\u003c/p\u003e\n\u003cp\u003eTickerQ addresses these demands using modern .NET primitives: source generators eliminate reflection, Entity Framework Core provides persistence, and SignalR powers real-time dashboards. Jobs are defined with attributes, generating boilerplate code at compile time. The scheduler is async-first, stateless at its core, and integrates seamlessly with ASP.NET Core\u0026rsquo;s dependency injection. The result: a framework that feels contemporary, performs efficiently, and surfaces errors early.\u003c/p\u003e\n\u003cp\u003eThe trade-off: as a newer entrant, the ecosystem and community remain smaller compared to long-established alternatives. For teams building new systems prioritizing performance and modern patterns, the architectural approach offers compelling advantages. For teams requiring battle-tested stability or extensive plugin ecosystems, maturity considerations become relevant.\u003c/p\u003e\n\u003cblockquote\u003e\n\n\n\n\n\u003ch2 id=\"disclaimer\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#disclaimer\" title=\"Disclaimer\"\u003eDisclaimer\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis article’s code examples reflect the TickerQ API as of versions 8+ (current docs show .NET 8+ usage). If you are on older major versions, please refer to the official upgrade notes and adapt signatures and configuration accordingly.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"architecture-source-generation-and-stateless-core\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#architecture-source-generation-and-stateless-core\" title=\"Architecture: Source Generation and Stateless Core\"\u003eArchitecture: Source Generation and Stateless Core\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s architecture centers on compile-time code generation. Jobs are defined as methods decorated with \u003ccode\u003e[TickerFunction]\u003c/code\u003e attributes. During compilation, source generators discover these methods, validate their signatures, and generate registration code that wires them into the scheduler without reflection.\u003c/p\u003e\n\u003cp\u003eConsider a job definition:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eReportJobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eReportJobs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TickerFunction(\u0026#34;GenerateMonthlyReport\u0026#34;, cronExpression: \u0026#34;0 0 0 1 * *\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateMonthlyReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTickerFunctionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGenerateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAt compile time, the source generator:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDiscovers the \u003ccode\u003eGenerateMonthlyReport\u003c/code\u003e method.\u003c/li\u003e\n\u003cli\u003eValidates the cron expression \u003ccode\u003e0 0 0 1 * *\u003c/code\u003e (monthly at midnight, 6-part cron).\u003c/li\u003e\n\u003cli\u003eGenerates registration code mapping \u003ccode\u003e\u0026quot;GenerateMonthlyReport\u0026quot;\u003c/code\u003e to the method.\u003c/li\u003e\n\u003cli\u003eInjects dependency resolution logic for \u003ccode\u003eIReportService\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAt runtime, the scheduler invokes jobs via generated delegates—no reflection, no \u003ccode\u003eMethodInfo.Invoke()\u003c/code\u003e, no dictionary lookups. This yields:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e: Reflection overhead eliminated, reducing invocation latency.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCompile-time safety\u003c/strong\u003e: Invalid cron expressions or missing dependencies cause build errors, not runtime exceptions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTooling support\u003c/strong\u003e: IDEs detect errors, provide IntelliSense, and enable refactoring tools to work correctly.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe stateless core design means job state lives in the database (via Entity Framework Core), not in-memory. The scheduler queries the database for jobs whose execution times have arrived, claims them atomically, and dispatches them to workers. This architecture supports clustering naturally: multiple instances coordinate via the database without custom locking logic.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-entity-framework-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#configuration-and-entity-framework-integration\" title=\"Configuration and Entity Framework Integration\"\u003eConfiguration and Entity Framework Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating TickerQ requires configuring Entity Framework Core for persistence. Install the packages:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package TickerQ\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package TickerQ.EntityFrameworkCore\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package TickerQ.Dashboard\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eConfigure services in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.DependencyInjection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.EntityFrameworkCore.DependencyInjection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Dashboard.DependencyInjection\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxConcurrency\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddOperationalStore\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedulerDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eefOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eefOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseApplicationDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedulerDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigurationType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseModelCustomizer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetBasePath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/tickerq/dashboard\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithBasicAuth\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eAddOperationalStore\u003c/code\u003e method integrates TickerQ with your existing \u003ccode\u003eDbContext\u003c/code\u003e. TickerQ creates tables for job definitions (\u003ccode\u003eTimeTicker\u003c/code\u003e, \u003ccode\u003eCronTicker\u003c/code\u003e) and execution history. Using \u003ccode\u003eUseApplicationDbContext\u0026lt;SchedulerDbContext\u0026gt;(ConfigurationType.UseModelCustomizer)\u003c/code\u003e applies TickerQ\u0026rsquo;s entity configurations via model customizer while keeping your domain model clean.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"generate-and-apply-migrations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#generate-and-apply-migrations\" title=\"Generate and apply migrations\"\u003eGenerate and apply migrations\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet ef migrations add AddTickerQSupport -c SchedulerDbContext\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet ef database update\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTickerQ\u0026rsquo;s tables store:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCronTickers\u003c/strong\u003e: Recurring jobs with cron expressions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTimeTickers\u003c/strong\u003e: One-time jobs scheduled for specific execution times.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCronTickerOccurrences\u003c/strong\u003e: Execution history for audit trails and retry tracking.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis database-backed persistence ensures jobs survive application restarts. If a job should have executed while the application was down, TickerQ handles it upon restart based on configuration—either executing missed jobs or skipping them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-definitions-cron-and-time-tickers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#job-definitions-cron-and-time-tickers\" title=\"Job Definitions: Cron and Time Tickers\"\u003eJob Definitions: Cron and Time Tickers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ supports two job types: cron-based recurring jobs and time-based one-time jobs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCron jobs\u003c/strong\u003e execute repeatedly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMaintenanceJobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TickerFunction(\u0026#34;CleanupLogs\u0026#34;, \u0026#34;0 0 * * * *\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eCleanupLogs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTickerFunctionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eDeleteOldLogsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe source generator validates \u003ccode\u003e\u0026quot;0 0 * * * *\u0026quot;\u003c/code\u003e at compile time. If the expression is invalid—say, \u003ccode\u003e\u0026quot;0 25 * * * *\u0026quot;\u003c/code\u003e (invalid hour)—the build fails with a descriptive error.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTime tickers\u003c/strong\u003e execute once at a specified time:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eNotificationJobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TickerFunction(\u0026#34;SendReminder\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eSendReminder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTickerFunctionContext\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSendReminderEmailAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSchedule time tickers programmatically:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Entities\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Base\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Interfaces.Managers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eITimeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;SendReminder\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHours\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRequest\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTickerHelper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateTickerRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetries\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetryIntervals\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"m\"\u003e60\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e300\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e900\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 1min, 5min, 15min\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis schedules a reminder to send in two hours. If execution fails, TickerQ retries up to three times with increasing intervals. The \u003ccode\u003eRequest\u003c/code\u003e parameter passes data (here, \u003ccode\u003euserId\u003c/code\u003e) to the job, serialized as JSON in the database.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-time-dashboard-with-signalr\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#real-time-dashboard-with-signalr\" title=\"Real-Time Dashboard with SignalR\"\u003eReal-Time Dashboard with SignalR\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s dashboard provides live visibility into job states using SignalR for real-time updates. Administrators view:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eActive jobs\u003c/strong\u003e: Currently executing, with elapsed time and progress indicators.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScheduled jobs\u003c/strong\u003e: Pending execution with countdown timers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExecution history\u003c/strong\u003e: Completed jobs with duration, outcome, and error details.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCron tickers\u003c/strong\u003e: Recurring jobs with last/next execution times.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe dashboard also supports:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eManual triggering\u003c/strong\u003e: Execute recurring jobs on-demand.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJob cancellation\u003c/strong\u003e: Stop long-running jobs mid-execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive updates\u003c/strong\u003e: Job states update in real-time via SignalR, no page refreshes required.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eConfigure basic authentication to protect the dashboard:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetBasePath\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/tickerq/dashboard\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003edashboardOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithBasicAuth\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor production deployments, integrate with your authentication system—ASP.NET Core Identity, OAuth, or Azure AD—using \u003ccode\u003eWithHostAuthentication()\u003c/code\u003e and standard ASP.NET Core authorization policies.\u003c/p\u003e\n\u003cp\u003eThe dashboard\u0026rsquo;s Vue.js-based UI is modern and responsive, tailored for operational teams monitoring background processing health. Compare this to Hangfire\u0026rsquo;s dashboard, which uses server-rendered HTML with periodic polling. TickerQ\u0026rsquo;s SignalR approach reduces latency and provides instant feedback when job states change.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"retry-policies-throttling-and-distributed-coordination\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#retry-policies-throttling-and-distributed-coordination\" title=\"Retry Policies, Throttling, and Distributed Coordination\"\u003eRetry Policies, Throttling, and Distributed Coordination\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ supports per-job retry policies:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Entities\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ImportData\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetries\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRetryIntervals\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e60\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e120\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e300\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e600\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Exponential backoff\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFailed jobs retry based on the specified intervals. After exhausting retries, jobs transition to \u003ccode\u003eFailed\u003c/code\u003e state, visible in the dashboard with full error details.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThrottling\u003c/strong\u003e limits concurrent execution. If your database supports 50 concurrent connections and you schedule 100 jobs simultaneously, throttling prevents connection exhaustion:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTickerQ\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxConcurrency\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Max 10 concurrent jobs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTickerQ queues excess jobs until workers become available, preventing resource contention.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDistributed coordination\u003c/strong\u003e works via Entity Framework Core\u0026rsquo;s optimistic concurrency. When a scheduler instance queries for jobs, it claims them with an atomic update:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eUPDATE\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTicker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSET\u003c/span\u003e \u003cspan class=\"n\"\u003eState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessing\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eInstance\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003eserver\u003c/span\u003e\u003cspan class=\"p\"\u003e-\u003c/span\u003e\u003cspan class=\"m\"\u003e01\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eWHERE\u003c/span\u003e \u003cspan class=\"n\"\u003eState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e\u003cspan class=\"n\"\u003ePending\u003c/span\u003e\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003e \u003cspan class=\"n\"\u003eAND\u003c/span\u003e \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"n\"\u003eGETUTCDATE\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOnly one instance succeeds per job. If an instance crashes mid-execution, orphaned jobs remain in \u003ccode\u003eProcessing\u003c/code\u003e state until a recovery mechanism detects and resets them—configurable via timeout policies.\u003c/p\u003e\n\u003cp\u003eThis coordination is simpler than Quartz.NET\u0026rsquo;s pessimistic locking but sufficient for most scenarios. Teams running dozens of instances in high-throughput environments may need to tune timeout settings to balance recovery speed and false-positive detection.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"batch-jobs-and-dependency-workflows\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#batch-jobs-and-dependency-workflows\" title=\"Batch Jobs and Dependency Workflows\"\u003eBatch Jobs and Dependency Workflows\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ supports batch jobs—groups of related tasks that execute as a unit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Entities\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTickerQ.Utilities.Interfaces.Managers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Schedule parent job\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eparentResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;ImportUsers\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eparentId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eparentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Schedule dependent job that runs only if parent succeeds\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_timeTickerManager\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeTickerEntity\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eFunction\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;TransformUsers\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eExecutionTime\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eParentId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eparentId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRunCondition\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRunCondition\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnSuccess\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTickerQ executes \u003ccode\u003eImportUsers\u003c/code\u003e first. If it succeeds, \u003ccode\u003eTransformUsers\u003c/code\u003e runs; if it fails, \u003ccode\u003eTransformUsers\u003c/code\u003e is skipped. This declarative workflow removes custom orchestration logic from application code.\u003c/p\u003e\n\u003cp\u003eBatch conditions include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAlways\u003c/strong\u003e: Execute regardless of parent outcomes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnSuccess\u003c/strong\u003e: Execute only if all previous batch jobs succeeded.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnFailure\u003c/strong\u003e: Execute only if any previous job failed (error handling workflows).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis feature mirrors Hangfire\u0026rsquo;s continuations and Quartz.NET\u0026rsquo;s job chaining but integrates more naturally with Entity Framework Core\u0026rsquo;s transactional boundaries.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-tickerq-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#when-tickerq-fits\" title=\"When TickerQ Fits\"\u003eWhen TickerQ Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance matters\u003c/strong\u003e: High job volumes benefit from reflection-free execution and async-first design.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eCompile-time safety is valued\u003c/strong\u003e: Teams that prefer catching configuration errors during builds rather than runtime.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eModern tooling is prioritized\u003c/strong\u003e: Source generation, SignalR, and Entity Framework Core integration appeal to teams comfortable with current .NET patterns.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eReal-time observability is required\u003c/strong\u003e: The dashboard\u0026rsquo;s live updates provide operational visibility without custom monitoring infrastructure.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTickerQ is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eBattle-tested stability is critical\u003c/strong\u003e: Hangfire (13+ years) and Quartz.NET (20+ years) have larger user bases and more production validation.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eExtensive plugins are needed\u003c/strong\u003e: TickerQ\u0026rsquo;s ecosystem is smaller. Hangfire and Quartz.NET offer more storage backends, monitoring integrations, and community extensions.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eLegacy .NET Framework support is required\u003c/strong\u003e: TickerQ targets modern .NET (6+). Teams on .NET Framework should use Hangfire or Quartz.NET.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-considerations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#operational-considerations\" title=\"Operational Considerations\"\u003eOperational Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s reliance on Entity Framework Core couples job scheduling to your database strategy. Teams already using EF Core benefit from unified migration workflows and tooling. Teams preferring Dapper, raw SQL, or NoSQL databases face friction—TickerQ\u0026rsquo;s operational store requires EF Core.\u003c/p\u003e\n\u003cp\u003eThe source generation approach requires recompilation when job definitions change. This aligns with modern CI/CD practices (deploy code, not configuration) but contrasts with Hangfire or Quartz.NET, where jobs can be scheduled dynamically at runtime without redeployment.\u003c/p\u003e\n\u003cp\u003eTickerQ\u0026rsquo;s dashboard consumes resources—SignalR connections, server memory for real-time updates. In resource-constrained environments, disable the dashboard and rely on application logging.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTickerQ represents modern .NET job scheduling: source generation, async-first design, and real-time monitoring. It bridges the gap between simplicity (NCronJob, Coravel) and enterprise features (Quartz.NET), offering persistence and performance without operational complexity.\u003c/p\u003e\n\u003cp\u003eConsider TickerQ if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYou\u0026rsquo;re building new systems on modern .NET (6+).\u003c/li\u003e\n\u003cli\u003ePerformance and compile-time safety are priorities.\u003c/li\u003e\n\u003cli\u003eYou use Entity Framework Core and value tooling integration.\u003c/li\u003e\n\u003cli\u003eReal-time dashboards enhance operational workflows.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid TickerQ if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour system runs on .NET Framework or older .NET Core versions.\u003c/li\u003e\n\u003cli\u003eYou need extensive ecosystem support or community plugins.\u003c/li\u003e\n\u003cli\u003eYou prefer runtime configuration over compile-time code generation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe final article synthesizes the series into comparative guidance, presenting a feature matrix, rating framework suitability across dimensions, and offering decision heuristics for selecting the right scheduler based on system maturity, infrastructure, and team priorities.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-11T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-6-tickerq/","language":"en","summary":"How TickerQ uses source generation, EF Core, and a real-time dashboard to deliver reflection-free, async-first scheduling for modern cloud-native systems.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — TickerQ and Modern Architecture","url":"https://daily-devops.net/posts/dotnet-job-scheduling-6-tickerq/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eWhen someone handed me \u003ca href=\"https://spinroot.com/gerard/pdf/P10.pdf\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGerard Holzmann\u0026rsquo;s \u0026ldquo;Power of Ten\u0026rdquo;\u003c/a\u003e rules and asked whether they still apply to C# 10/.NET, my answer was immediate: Absolutely, and we can enforce them better than Gerard Holzmann could have dreamed in 2006. The Power of Ten originated at NASA\u0026rsquo;s Jet Propulsion Laboratory for safety-critical C code in spacecraft systems. Not academic theory—principles where violations kill people. NASA\u0026rsquo;s \u003ca href=\"https://users.ece.cmu.edu/~koopman/pubs/koopman14_toyota_ua_slides.pdf\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e2010 technical assessment of Toyota\u0026rsquo;s electronic throttle control\u003c/a\u003e found 243 violations contributing to unintended acceleration deaths. When prosecutors examine catastrophic embedded failures, these ten rules are the baseline for \u0026ldquo;did you even try?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eWhat\u0026rsquo;s changed in two decades? In 2006, following these rules meant discipline, manual code reviews, and static analysis tools at $5,000 per seat. You verified bounded loops by hand, tracked pointer indirection with spreadsheets, enforced function length through policy documents nobody read. I\u0026rsquo;ve reviewed enough legacy embedded code to know most teams failed. The tooling wasn\u0026rsquo;t there, and manual enforcement doesn\u0026rsquo;t scale past three developers. Modern C# flips this completely. Roslyn analyzers catch violations at compile time—before code review, before testing, before anyone debugs at 2 AM wondering why the production system locked up. Nullable reference types enforce null-safety that C developers achieved through religious discipline and hope. The type system makes pointer arithmetic bugs physically impossible. Built into the compiler, zero additional cost, enforced automatically.\u003c/p\u003e\n\u003cp\u003eThe catch—and there\u0026rsquo;s always a catch—is that not every rule translates directly. The managed runtime fundamentally rewrites what\u0026rsquo;s dangerous and what\u0026rsquo;s safe. Dynamic allocation is forbidden in embedded systems but powers every .NET framework feature. Recursion crashes spacecraft but handles expression trees beautifully when the CLR manages your stack. The real question isn\u0026rsquo;t \u0026ldquo;do these rules apply to C#?\u0026rdquo; It\u0026rsquo;s \u0026ldquo;how do modern language features enforce the underlying principles better than 2006 C ever could?\u0026rdquo; Let\u0026rsquo;s examine each rule systematically.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-1-avoid-complex-control-flow-no-goto-setjmplongjmp-recursion\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-1-avoid-complex-control-flow-no-goto-setjmplongjmp-recursion\" title=\"Rule 1: Avoid Complex Control Flow (No goto, setjmp/longjmp, Recursion)\"\u003eRule 1: Avoid Complex Control Flow (No goto, setjmp/longjmp, Recursion)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Simplify static analysis and prevent unpredictable control flow in embedded systems.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Partially valid, but context matters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-still-applies\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#what-still-applies\" title=\"What Still Applies\"\u003eWhat Still Applies\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003egoto\u003c/code\u003e remains controversial in C#, though the language supports it. I\u0026rsquo;ve seen developers defend it passionately in code reviews, and honestly, for breaking out of deeply nested loops, it\u0026rsquo;s occasionally the least-bad option. But \u0026ldquo;occasionally\u0026rdquo; is doing a lot of work in that sentence:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Acceptable use case: breaking out of nested loops\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryFindPosition\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[,]\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003erow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecol\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eposition\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003erows\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ecols\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003egoto\u003c/span\u003e \u003cspan class=\"n\"\u003efound\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003efound\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBut this is clearer:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Better: extract to method with early return\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryFindPosition\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[,]\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003erow\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecol\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003eposition\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetLength\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ematrix\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003eposition\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eposition\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"whats-different\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#whats-different\" title=\"What\u0026rsquo;s Different\"\u003eWhat\u0026rsquo;s Different\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eRecursion\u003c/strong\u003e tells a completely different story. The JIT compiler doesn\u0026rsquo;t guarantee tail-call optimization like F# does. Still valuable for tree structures, parsing, algorithms where iterative alternatives become unreadable spaghetti. Key difference? Stack overflows throw exceptions you can catch and handle. In embedded C, stack overflow crashes the spacecraft. No recovery, no logging, just silence.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Recursion is perfectly acceptable for bounded structures\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeft\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRight\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor deep recursion, modern C# offers alternatives:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Convert to iteration with explicit stack\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eFind\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStack\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTreeNode\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePop\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRight\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRight\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeft\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003estack\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePush\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLeft\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Use recursion where it makes sense, but be aware of stack depth. Avoid \u003ccode\u003egoto\u003c/code\u003e unless you have a compelling reason. The .NET runtime gives you safety nets that embedded C never had.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-2-all-loops-must-have-fixed-upper-bounds\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-2-all-loops-must-have-fixed-upper-bounds\" title=\"Rule 2: All Loops Must Have Fixed Upper Bounds\"\u003eRule 2: All Loops Must Have Fixed Upper Bounds\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Enable static analysis to prove termination and prevent infinite loops.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e The principle is sound, but enforcement differs dramatically.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"modern-interpretation\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-interpretation\" title=\"Modern Interpretation\"\u003eModern Interpretation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eProvable termination is still the goal, but .NET gives you runtime safety nets that embedded C never had:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: unbounded loop\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eitem\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDequeue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"c1\"\u003e// What if queue is empty?\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Better: explicit bound with timeout\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ects\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationTokenSource\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003ects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToken\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsCancellationRequested\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryDequeue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitem\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ects\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eModern .NET provides powerful static analysis through \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e that can detect unbounded loops during compilation. Enable nullable reference types and the latest analysis level to catch these issues early:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;Nullable\u0026gt;\u003c/span\u003eenable\u003cspan class=\"nt\"\u003e\u0026lt;/Nullable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisLevel\u0026gt;\u003c/span\u003elatest\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisLevel\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Always use bounded loops. Modern C# makes this easier with \u003ccode\u003eforeach\u003c/code\u003e, LINQ\u0026rsquo;s \u003ccode\u003eTake()\u003c/code\u003e, and cancellation tokens. The difference from embedded C: Your loops can be bounded at runtime with safe defaults rather than requiring compile-time constants.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-3-no-dynamic-memory-allocation-after-initialization\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-3-no-dynamic-memory-allocation-after-initialization\" title=\"Rule 3: No Dynamic Memory Allocation After Initialization\"\u003eRule 3: No Dynamic Memory Allocation After Initialization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Prevent memory exhaustion and fragmentation in systems without virtual memory.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Fundamentally incompatible with .NET\u0026rsquo;s design philosophy.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-rule-doesnt-translate\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#why-this-rule-doesnt-translate\" title=\"Why This Rule Doesn\u0026rsquo;t Translate\"\u003eWhy This Rule Doesn\u0026rsquo;t Translate\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe .NET garbage collector exists precisely to enable safe dynamic allocation. Forbidding dynamic allocation in .NET is like forbidding breathing—every framework feature depends on it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Standard .NET patterns rely on GC\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresults\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStringAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eparsed\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDeserialize\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eMyData\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efiltered\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eparsed\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsActive\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eToList\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-modern-equivalent-minimize-allocations\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-modern-equivalent-minimize-allocations\" title=\"The Modern Equivalent: Minimize Allocations\"\u003eThe Modern Equivalent: Minimize Allocations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRather than avoiding allocation entirely, focus on reducing unnecessary allocations and GC pressure:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Inefficient: allocates multiple intermediate strings\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;Item {i}; \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Allocates new string each iteration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Efficient: single allocation, reusable buffer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esb\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecapacity\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e15\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Item \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAppend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;; \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"stack-allocation-for-performance-critical-code\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#stack-allocation-for-performance-critical-code\" title=\"Stack Allocation for Performance-Critical Code\"\u003eStack Allocation for Performance-Critical Code\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen allocation overhead matters, use \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e with \u003ccode\u003estackalloc\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Stack allocation for small, temporary buffers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003estackalloc\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e256\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eComputeValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eCritical constraints:\u003c/strong\u003e Stack allocations come with three important limitations. First, analyzer rule \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2014\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA2014\u003c/a\u003e prevents using \u003ccode\u003estackalloc\u003c/code\u003e inside loops to avoid stack overflow. Second, keep allocations small (under 1KB recommended) since stack space is limited. Third, \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e can\u0026rsquo;t escape method scope: attempting to return it produces compiler error \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/ref-safety-errors\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCS8347\u003c/a\u003e. For data that needs to escape, use \u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e or arrays instead.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"object-pooling-for-hot-paths\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#object-pooling-for-hot-paths\" title=\"Object Pooling for Hot Paths\"\u003eObject Pooling for Hot Paths\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor high-throughput scenarios:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Use ArrayPool for reusable buffers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eArrayPool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eShared\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e4096\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ebytesRead\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReadAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsMemory\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e4096\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProcessBytes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ebytesRead\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efinally\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArrayPool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eShared\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReturn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Don\u0026rsquo;t avoid allocation, manage it intelligently. Use \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e/\u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e for performance-critical code, \u003ccode\u003eArrayPool\u0026lt;T\u0026gt;\u003c/code\u003e for reusable buffers, and profile before optimizing. I\u0026rsquo;ve watched teams waste months optimizing allocations that consumed 2% of execution time while ignoring the database query running on every keystroke. The GC is your friend, not your enemy—but measure before you fight it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-4-no-function-longer-than-one-printed-page-60-lines\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-4-no-function-longer-than-one-printed-page-60-lines\" title=\"Rule 4: No Function Longer Than One Printed Page (~60 Lines)\"\u003eRule 4: No Function Longer Than One Printed Page (~60 Lines)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Keep functions digestible for human review and static analysis.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Absolutely valid, possibly even more important.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-rule-still-matters\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#why-this-rule-still-matters\" title=\"Why This Rule Still Matters\"\u003eWhy This Rule Still Matters\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn 15 years across finance, healthcare, and industrial control systems, I\u0026rsquo;ve never once regretted breaking a long method into smaller pieces. I\u0026rsquo;ve regretted plenty of 500-line monsters—particularly the one in a reporting visualization that took three developers two weeks to debug because nobody could hold the entire state machine in their head:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: 250-line method doing everything\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Validate customer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Check inventory\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Calculate pricing\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Apply discounts\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Process payment\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Update inventory\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Send notifications\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Log everything\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Handle 12 different error cases\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... 200 more lines\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eModern C# actually makes this rule easier to enforce:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: orchestration method with clear intent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eavailability\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eCheckInventory\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eavailability\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAllAvailable\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOutOfStock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eavailability\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnavailableItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epricing\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculatePricing\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTier\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epayment\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epricing\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSucceeded\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentFailed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReason\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eUpdateInventory\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSendConfirmation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTransactionId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"modern-enforcement-tools\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-enforcement-tools\" title=\"Modern Enforcement Tools\"\u003eModern Enforcement Tools\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnlike 2006 C, modern tooling enforces this automatically. The \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1505\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1505 analyzer rule\u003c/a\u003e flags unmaintainable code based on cyclomatic complexity and maintainability index. Configure it in \u003ccode\u003e.editorconfig\u003c/code\u003e to treat violations as warnings, ensuring your team maintains digestible function sizes without manual review.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLocal functions\u003c/strong\u003e reduce the need for small private methods:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIEnumerable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetRecentOrders\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ecutoffDate\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddDays\u003c/span\u003e\u003cspan class=\"p\"\u003e(-\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrders\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWhere\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIsRecent\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderByDescending\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eIsRecent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDate\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003ecutoffDate\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e If anything, aim for shorter than 60 lines. With expression-bodied members, local functions, and LINQ, there\u0026rsquo;s no excuse for bloated methods. Enable analyzers to enforce it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-5-assertion-density-of-two-per-function\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-5-assertion-density-of-two-per-function\" title=\"Rule 5: Assertion Density of Two Per Function\"\u003eRule 5: Assertion Density of Two Per Function\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Catch anomalous conditions early with runtime checks.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Valid principle, but modern C# offers better mechanisms.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-modern-equivalent-multiple-layers-of-defense\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-modern-equivalent-multiple-layers-of-defense\" title=\"The Modern Equivalent: Multiple Layers of Defense\"\u003eThe Modern Equivalent: Multiple Layers of Defense\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNASA wanted two runtime assertions per function. C# gives you something better—multiple enforcement layers, starting at compile time:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1. Compile-Time Checks: Nullable Reference Types\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderProcessor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Compiler enforces non-null at compile time\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// No runtime null check needed - compiler guarantees non-null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etotal\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePrice\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Nullable warns if customer.Email might be null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eSendReceipt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSendReceipt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eemail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// email is guaranteed non-null by caller contract\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e2. Parameter Validation: Guard Clauses\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e \u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// .NET 6+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgumentOutOfRangeException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNegativeOrZero\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Business logic here\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Pre-.NET 6 equivalent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e \u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emethod\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emethod\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentOutOfRangeException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eamount\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Amount must be positive\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Business logic here\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e3. Debug Assertions for Internal Invariants\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Diagnostics\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eUpdateBalance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAccount\u003c/span\u003e \u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003edelta\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Account should never be null here\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsClosed\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Should not update closed accounts\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBalance\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003edelta\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBalance\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Balance should never go negative\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eKey difference:\u003c/strong\u003e \u003ccode\u003eDebug.Assert\u003c/code\u003e is removed in Release builds, making it suitable for checking invariants during development without runtime cost. For mission-critical code, consider \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/framework/debug-trace-profile/code-contracts\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCode Contracts\u003c/a\u003e which provide formal preconditions, postconditions, and object invariants with static verification support.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"pattern-matching-for-exhaustiveness\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#pattern-matching-for-exhaustiveness\" title=\"Pattern Matching for Exhaustiveness\"\u003ePattern Matching for Exhaustiveness\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern C# can enforce completeness at compile time:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetStatusMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003estatus\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePending\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order is pending\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessing\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order is being processed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eShipped\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order has been shipped\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelivered\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order delivered\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eOrderStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCancelled\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order cancelled\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Compiler error if any enum value is missing (with warnings enabled)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Use nullable reference types for compile-time null safety, guard clauses for parameter validation, and \u003ccode\u003eDebug.Assert\u003c/code\u003e for invariants. This gives you better than \u0026ldquo;two assertions per function\u0026rdquo;: you get enforcement at compile time where possible, and runtime checks where needed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-6-declare-data-at-smallest-possible-scope\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-6-declare-data-at-smallest-possible-scope\" title=\"Rule 6: Declare Data at Smallest Possible Scope\"\u003eRule 6: Declare Data at Smallest Possible Scope\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Minimize variable lifetime and potential misuse.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Completely valid and reinforced by modern language features.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"modern-c-makes-this-easier\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-c-makes-this-easier\" title=\"Modern C# Makes This Easier\"\u003eModern C# Makes This Easier\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: variables declared too early\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ecount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eaverage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 50 lines later...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ecount\u003c/span\u003e\u003cspan class=\"p\"\u003e++;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eaverage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003ecount\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Another 30 lines...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMax\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: declare at point of use\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... other logic ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eaverage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e/\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... other logic ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emax\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMax\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"pattern-matching-limits-scope-automatically\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#pattern-matching-limits-scope-automatically\" title=\"Pattern Matching Limits Scope Automatically\"\u003ePattern Matching Limits Scope Automatically\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Scope limited to when pattern matches\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eobj\u003c/span\u003e \u003cspan class=\"k\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsActive\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// \u0026#39;customer\u0026#39; only exists in this block\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eProcessCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// \u0026#39;customer\u0026#39; doesn\u0026#39;t exist here\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Switch expressions with declaration patterns\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ediscount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003elargeOrder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elargeOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e0.1\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsPremium\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003epremiumOrder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003epremiumOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e0.05\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"using-declarations-for-resource-management\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#using-declarations-for-resource-management\" title=\"Using Declarations for Resource Management\"\u003eUsing Declarations for Resource Management\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: resource scope too broad\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eReadConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOpenRead\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;config.json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 100 lines of code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eReadFromStream\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efinally\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDispose\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: using declaration limits scope\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eReadConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOpenRead\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;config.json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eReadFromStream\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// stream disposed here, not at method end\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Even better: compact using declaration (C# 8+)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eReadConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003estream\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eFile\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOpenRead\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;config.json\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eReadFromStream\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// stream disposed at end of method automatically\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e More relevant than ever. Pattern matching limits scope automatically. Using declarations prevent resource leaks. Block-scoped variables can\u0026rsquo;t escape their intended lifetime. C# 10\u0026rsquo;s file-scoped namespaces extend this principle to namespace declarations—one less level of indentation, one less place for variables to hide.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-7-check-return-values-and-parameter-validity\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-7-check-return-values-and-parameter-validity\" title=\"Rule 7: Check Return Values and Parameter Validity\"\u003eRule 7: Check Return Values and Parameter Validity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Never ignore potential failures; validate all inputs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Absolutely critical, but mechanisms differ.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"return-value-checking-exceptions-vs-result-types\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#return-value-checking-exceptions-vs-result-types\" title=\"Return Value Checking: Exceptions vs. Result Types\"\u003eReturn Value Checking: Exceptions vs. Result Types\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET uses exceptions for error conditions, unlike C\u0026rsquo;s return codes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// .NET idiomatic: exception-based\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSaveCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveChanges\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Throws on error\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDbUpdateException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Failed to save customer {CustomerId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Re-throw or wrap in domain exception\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBut C#\u0026rsquo;s \u003ccode\u003eTry*\u003c/code\u003e pattern provides explicit success/failure:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// When failure is expected and not exceptional\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einput\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Invalid number format\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003einventory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryReserve\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eproductId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003equantity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ereservation\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInsufficientStock\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"modern-pattern-result-types\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-pattern-result-types\" title=\"Modern Pattern: Result Types\"\u003eModern Pattern: Result Types\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen failure is a normal business outcome rather than an exceptional condition, exceptions feel wrong. Result types make success and failure explicit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003einit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003einit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eError\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003einit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eError\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eerror\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ePlaceOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003evalidationResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e!);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessPayment\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eFail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e!);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epaymentResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e!));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"parameter-validation-multiple-strategies\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#parameter-validation-multiple-strategies\" title=\"Parameter Validation: Multiple Strategies\"\u003eParameter Validation: Multiple Strategies\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 1. Constructor validation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIOrderRepository\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_repository\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 2. Method parameter validation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eCreateOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderItem\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Order must contain at least one item\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQuantity\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;All items must have positive quantity\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Business logic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"analyzer-support\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#analyzer-support\" title=\"Analyzer Support\"\u003eAnalyzer Support\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRoslyn analyzers automatically detect unchecked return values. Enable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1806\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1806\u003c/a\u003e to catch ignored method results and \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0058\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eIDE0058\u003c/a\u003e to flag unused expression values. These analyzers ensure your team never silently discards important return information.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e Always validate parameters. Always handle exceptions or check return values. Use \u003ccode\u003eArgumentNullException.ThrowIfNull()\u003c/code\u003e for parameter checks, \u003ccode\u003eTry*\u003c/code\u003e methods for expected failures, and exceptions for exceptional conditions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-8-limited-preprocessor-use\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-8-limited-preprocessor-use\" title=\"Rule 8: Limited Preprocessor Use\"\u003eRule 8: Limited Preprocessor Use\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Avoid complex macros that obscure code meaning and hinder analysis.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Largely irrelevant: C# preprocessor is far more constrained.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-c-doesnt-have-thank-goodness\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#what-c-doesnt-have-thank-goodness\" title=\"What C# Doesn\u0026rsquo;t Have (Thank Goodness)\"\u003eWhat C# Doesn\u0026rsquo;t Have (Thank Goodness)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eC# preprocessor directives can\u0026rsquo;t:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDefine function-like macros\u003c/li\u003e\n\u003cli\u003ePerform token pasting\u003c/li\u003e\n\u003cli\u003eCreate recursive macros\u003c/li\u003e\n\u003cli\u003eHide complex logic\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// This is NOT possible in C#:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// #define MAX(a, b) ((a) \u0026gt; (b) ? (a) : (b))   // No function-like macros\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// What C# DOES support:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#define\u003c/span\u003e \u003cspan class=\"n\"\u003eDEBUG_LOGGING\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003eDEBUG_LOGGING\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Debug information\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-real-concern-conditional-compilation-abuse\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-real-concern-conditional-compilation-abuse\" title=\"The Real Concern: Conditional Compilation Abuse\"\u003eThe Real Concern: Conditional Compilation Abuse\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Bad: excessive conditional compilation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eConfigService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003ePRODUCTION\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Production connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#elif\u003c/span\u003e \u003cspan class=\"n\"\u003eSTAGING\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Staging connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#elif\u003c/span\u003e \u003cspan class=\"n\"\u003eDEVELOPMENT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Development connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#else\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Local connection string\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Good: configuration-based approach\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eConfigService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIConfiguration\u003c/span\u003e \u003cspan class=\"n\"\u003e_config\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIConfiguration\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_config\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_config\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetConnectionString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DefaultConnection\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"legitimate-uses\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#legitimate-uses\" title=\"Legitimate Uses\"\u003eLegitimate Uses\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Acceptable: Debug-only code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003eDEBUG\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Production code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esum\u003c/span\u003e \u003cspan class=\"p\"\u003e+=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Acceptable: Platform-specific code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#if\u003c/span\u003e \u003cspan class=\"n\"\u003eWINDOWS\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DllImport(\u0026#34;user32.dll\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003eextern\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eShowWindow\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIntPtr\u003c/span\u003e \u003cspan class=\"n\"\u003ehWnd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003enCmdShow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#elif\u003c/span\u003e \u003cspan class=\"n\"\u003eLINUX\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DllImport(\u0026#34;libX11.so\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003eextern\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eXOpenDisplay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003edisplay_name\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#endif\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e C#\u0026rsquo;s preprocessor is already minimal by design. Use it for debug-only code and platform-specific implementations. For everything else, use dependency injection and configuration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-9-restrict-pointer-use-max-one-level-of-dereferencing\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-9-restrict-pointer-use-max-one-level-of-dereferencing\" title=\"Rule 9: Restrict Pointer Use (Max One Level of Dereferencing)\"\u003eRule 9: Restrict Pointer Use (Max One Level of Dereferencing)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Prevent complex pointer arithmetic and double-pointer confusion.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Mostly irrelevant: managed code eliminates most pointer usage.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-net-alternative-references-are-not-pointers\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-net-alternative-references-are-not-pointers\" title=\"The .NET Alternative: References Are Not Pointers\"\u003eThe .NET Alternative: References Are Not Pointers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn managed C#, you work with references, not pointers:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Safe by default - no pointer arithmetic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Reference semantics, but no pointer manipulation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;John\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003esame\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// same reference, not a copy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esame\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Jane\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Outputs: Jane\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"unsafe-code-when-you-actually-need-pointers\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#unsafe-code-when-you-actually-need-pointers\" title=\"Unsafe Code: When You Actually Need Pointers\"\u003eUnsafe Code: When You Actually Need Pointers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRare, but sometimes unavoidable for interop or extreme performance:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Performance-critical unsafe code - use sparingly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eunsafe\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efixed\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eptr\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// One level of indirection - NASA would approve\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eptr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e)(*\u003c/span\u003e\u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ecurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e++;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"modern-safe-alternative-span\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-safe-alternative-span\" title=\"Modern Safe Alternative: Span\"\u003eModern Safe Alternative: Span\u003cT\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Safe, zero-allocation, pointer-like performance\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Or even better:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eref\u003c/span\u003e \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e)(\u003c/span\u003e\u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"refinout-managed-pointer-like-semantics\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#refinout-managed-pointer-like-semantics\" title=\"ref/in/out: Managed \u0026ldquo;Pointer-Like\u0026rdquo; Semantics\"\u003eref/in/out: Managed \u0026ldquo;Pointer-Like\u0026rdquo; Semantics\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Pass by reference without pointers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eUpdateCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eref\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Updated\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Read-only reference (no copying large structs)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eLargeStruct\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue1\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValue2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// No defensive copy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Output parameter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryGetCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e You rarely need unsafe code in modern C#. Use \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e/\u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e for performance-critical buffer manipulation. Use \u003ccode\u003eref\u003c/code\u003e/\u003ccode\u003ein\u003c/code\u003e/\u003ccode\u003eout\u003c/code\u003e for reference semantics. Reserve \u003ccode\u003eunsafe\u003c/code\u003e for true interop scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"rule-10-enable-all-compiler-warnings-and-use-static-analysis\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#rule-10-enable-all-compiler-warnings-and-use-static-analysis\" title=\"Rule 10: Enable All Compiler Warnings and Use Static Analysis\"\u003eRule 10: Enable All Compiler Warnings and Use Static Analysis\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eOriginal Intent:\u003c/strong\u003e Catch bugs early through automated checking.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC#/.NET Applicability:\u003c/strong\u003e Not just valid: vastly more powerful than 2006 C tooling.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"modern-tooling-is-incomparably-better\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#modern-tooling-is-incomparably-better\" title=\"Modern Tooling Is Incomparably Better\"\u003eModern Tooling Is Incomparably Better\u003c/a\u003e\u003c/h3\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=\"c\"\u003e\u0026lt;!-- Essential .csproj settings --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Treat all warnings as errors --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;TreatWarningsAsErrors\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/TreatWarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable nullable reference types --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;Nullable\u0026gt;\u003c/span\u003eenable\u003cspan class=\"nt\"\u003e\u0026lt;/Nullable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable latest code analysis --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisLevel\u0026gt;\u003c/span\u003elatest\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisLevel\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;EnforceCodeStyleInBuild\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/EnforceCodeStyleInBuild\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable specific analyzer categories --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisMode\u0026gt;\u003c/span\u003eAll\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisMode\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"roslyn-analyzers-static-analysis-built-in\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#roslyn-analyzers-static-analysis-built-in\" title=\"Roslyn Analyzers: Static Analysis Built In\"\u003eRoslyn Analyzers: Static Analysis Built In\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnlike 2006 C compilers, \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/code-quality/roslyn-analyzers-overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e provide deep semantic analysis. Rules like \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2000\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA2000\u003c/a\u003e catch undisposed resources, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1806\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1806\u003c/a\u003e flags ignored return values, and \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1062\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1062\u003c/a\u003e enforces parameter validation: all without running your code. This compile-time safety net would have been science fiction to NASA\u0026rsquo;s 2006 C developers.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"editorconfig-for-team-standards\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#editorconfig-for-team-standards\" title=\"EditorConfig for Team Standards\"\u003eEditorConfig for Team Standards\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://editorconfig.org/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eEditorConfig files\u003c/a\u003e let you enforce code style and quality rules across your team. Beyond basic formatting, you can control diagnostic severity for specific analyzer rules: turning \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1062\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1062\u003c/a\u003e (validate arguments) into build errors while suppressing overly strict rules like \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1303\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1303\u003c/a\u003e (localized strings). This ensures consistent standards without lengthy code review discussions about style preferences.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"multiple-layers-of-analysis\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#multiple-layers-of-analysis\" title=\"Multiple Layers of Analysis\"\u003eMultiple Layers of Analysis\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern .NET provides defense in depth: compiler warnings catch syntax issues, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e enforce code quality (\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA* rules\u003c/a\u003e) and style (\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eIDE* rules\u003c/a\u003e). Extend this with third-party analyzers like \u003ca href=\"https://security-code-scan.github.io/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eSecurityCodeScan\u003c/a\u003e for security vulnerabilities, \u003ca href=\"https://github.com/semihokur/AsyncFixer\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAsyncFixer\u003c/a\u003e for async/await pitfalls, or \u003ca href=\"https://www.nuget.org/packages/Microsoft.VisualStudio.Threading.Analyzers/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMicrosoft.VisualStudio.Threading.Analyzers\u003c/a\u003e for threading issues. For enterprise scenarios, \u003ca href=\"https://www.sonarsource.com/products/sonarqube/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eSonarQube\u003c/a\u003e and \u003ca href=\"https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub Advanced Security\u003c/a\u003e with CodeQL provide continuous security scanning.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"automated-code-review\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#automated-code-review\" title=\"Automated Code Review\"\u003eAutomated Code Review\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIntegrate analysis into CI/CD pipelines with \u003ca href=\"https://docs.github.com/en/actions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHub Actions\u003c/a\u003e, \u003ca href=\"https://learn.microsoft.com/en-us/azure/devops/pipelines/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAzure Pipelines\u003c/a\u003e, or similar platforms. Configure builds to treat warnings as errors (\u003ccode\u003e/p:TreatWarningsAsErrors=true\u003c/code\u003e) so quality issues block merges automatically. This transforms static analysis from optional developer tooling into enforced team standards.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eVerdict:\u003c/strong\u003e This is the one rule where C# developers have it absurdly better than 2006 C developers. I\u0026rsquo;ve reviewed codebases where enabling \u003ccode\u003eTreatWarningsAsErrors\u003c/code\u003e found 47 bugs in 30 seconds—bugs that had been sitting there for months. Enable everything: compiler warnings, Roslyn analyzers, nullable reference types. Use \u003ccode\u003e.editorconfig\u003c/code\u003e to enforce team standards. Automate it in CI/CD so quality gates can\u0026rsquo;t be ignored when the deadline looms. There\u0026rsquo;s no excuse not to.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"summary-translation-guide\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#summary-translation-guide\" title=\"Summary: Translation Guide\"\u003eSummary: Translation Guide\u003c/a\u003e\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003ePower of Ten Rule\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eC#/.NET Status\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eModern Equivalent\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e1. Simple control flow\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartially valid\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAvoid \u003ccode\u003egoto\u003c/code\u003e; recursion OK with care\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2. Bounded loops\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eValid\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUse \u003ccode\u003eforeach\u003c/code\u003e, LINQ \u003ccode\u003eTake()\u003c/code\u003e, \u003ccode\u003eCancellationToken\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e3. No dynamic allocation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNot applicable\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMinimize allocations with \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eArrayPool\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e4. Max 60 lines per function\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAbsolutely valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEnable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1505\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1505\u003c/a\u003e, use local functions, extract methods\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e5. Two assertions per function\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eValid principle\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNullable types\u003c/a\u003e + guard clauses + \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.assert\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eDebug.Assert\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e6. Minimal variable scope\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAbsolutely valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePattern matching, using declarations, block scope\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e7. Check all returns\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eAbsolutely valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eValidate parameters, handle exceptions, use \u003ccode\u003eTry*\u003c/code\u003e pattern\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e8. Limited preprocessor\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMostly irrelevant\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eC# preprocessor already constrained; use DI for config\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e9. Restrict pointers\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMostly irrelevant\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUse \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.span-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e, \u003ccode\u003eref\u003c/code\u003e/\u003ccode\u003ein\u003c/code\u003e/\u003ccode\u003eout\u003c/code\u003e; reserve \u003ccode\u003eunsafe\u003c/code\u003e for interop\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e10. All warnings + static analysis\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eEmphatically valid\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEnable \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eall analyzers\u003c/a\u003e, nullable types, treat warnings as errors\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\n\n\u003ch2 id=\"the-verdict-timeless-principles-modern-implementation\"\u003e\u003ca href=\"/posts/dotnet-power-of-ten-rules/#the-verdict-timeless-principles-modern-implementation\" title=\"The Verdict: Timeless Principles, Modern Implementation\"\u003eThe Verdict: Timeless Principles, Modern Implementation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFour rules (4, 6, 7, and 10) transfer directly with superior tooling. Function length limits, minimal scope, return value checking, static analysis—all work better in 2025 than 2006 C. Three rules (3, 8, 9) become largely irrelevant because .NET\u0026rsquo;s managed runtime provides better abstractions than manual memory management or preprocessor macros. The remaining rules (1, 2, 5) require contextual interpretation. Their principles remain sound, but modern C#\u0026rsquo;s language features and runtime safety nets fundamentally change implementation.\u003c/p\u003e\n\u003cp\u003eGerard Holzmann\u0026rsquo;s rules weren\u0026rsquo;t about C syntax. They encoded deeper principles—predictability, analyzability, defensive programming—that transcend any specific language. What\u0026rsquo;s different in 2025? Modern C# gives you powerful tools to enforce these principles without bare-metal constraints. \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eRoslyn analyzers\u003c/a\u003e catch bugs at compile time that required runtime assertions in embedded C. \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNullable reference types\u003c/a\u003e prevent null dereferences before code runs. \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.span-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e delivers pointer-like performance with array-like safety.\u003c/p\u003e\n\u003cp\u003eFor safety-critical C# code—medical devices, financial systems, industrial control—absolutely adopt these principles. Enable every analyzer. Enforce short functions. Validate everything. Treat warnings as errors. But don\u0026rsquo;t cargo-cult embedded C constraints just because NASA used them. Embrace the GC when it makes sense, use modern abstractions that eliminate entire bug categories, and let the type system catch errors at compile time instead of in production. Your code will be safer, more maintainable, and more correct. Which is what Gerard Holzmann wanted all along.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-10T16:45:00+01:00","id":"https://daily-devops.net/posts/dotnet-power-of-ten-rules/","language":"en","summary":"Holzmann's safety-critical coding rules hit harder in modern C#: Roslyn analyzers, nullable types, and the type system enforce what C only wished.\n","tags":["csharp","dotnet","bestpractices","codequality","softwareengineering"],"title":"Power of Ten Rules: More Relevant Than Ever for .NET","url":"https://daily-devops.net/posts/dotnet-power-of-ten-rules/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour microservice runs in a Kubernetes cluster, processing events from a message queue. Every hour, it purges stale cache entries. Every morning at 6 AM, it triggers a health check against downstream services. The service is stateless, ephemeral, and designed to scale horizontally based on load—we\u0026rsquo;ve seen it scale from 3 pods to 45 during traffic spikes.\u003c/p\u003e\n\u003cp\u003eYou need scheduled tasks, but adding a database for job persistence? That violates the entire stateless design principle. External schedulers like Kubernetes CronJobs? We tried that. Managing separate YAML manifests, container image versioning, and lifecycle coupling between the CronJob and the main deployment became a maintenance nightmare. When we needed to update the health check logic, we had to deploy both the main service \u003cem\u003eand\u003c/em\u003e the CronJob separately. Teams kept forgetting the second step.\u003c/p\u003e\n\u003cp\u003eNCronJob addresses this by embedding scheduling directly into the application using ASP.NET Core\u0026rsquo;s \u003ccode\u003eIHostedService\u003c/code\u003e. Jobs run in-process, require zero external dependencies, and scale with the application. The scheduler is lightweight—hundreds of lines of code, not thousands—and integrates seamlessly with dependency injection. The trade-off: jobs don\u0026rsquo;t persist, horizontal scaling can cause duplication, and advanced features like clustering or dashboards don\u0026rsquo;t exist.\u003c/p\u003e\n\u003cp\u003eFor microservices, containerized applications, or systems prioritizing simplicity over feature richness, NCronJob removes friction. For applications needing persistence, retry policies, or distributed coordination, alternative architectural approaches merit evaluation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architecture-ihostedservice-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#architecture-ihostedservice-integration\" title=\"Architecture: IHostedService Integration\"\u003eArchitecture: IHostedService Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob builds on \u003ccode\u003eIHostedService\u003c/code\u003e, the ASP.NET Core primitive for long-running background operations. When the application starts, NCronJob\u0026rsquo;s hosted service initializes, parses cron expressions, calculates next execution times, and schedules jobs using \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e. When execution times arrive, the scheduler invokes jobs via dependency injection, passing parameters if configured.\u003c/p\u003e\n\u003cp\u003eThe design is intentionally minimal. There\u0026rsquo;s no database, no external storage, no worker coordination beyond single-process execution. Jobs are defined as classes implementing \u003ccode\u003eIJob\u003c/code\u003e or via inline lambda expressions. The scheduler maintains an in-memory list of job definitions and fires them based on cron schedules.\u003c/p\u003e\n\u003cp\u003eThis simplicity makes NCronJob ideal for:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMicroservices\u003c/strong\u003e: Each service schedules its own tasks without shared infrastructure.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eContainerized deployments\u003c/strong\u003e: Stateless containers start, execute scheduled tasks, and terminate without persisting job state.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInternal tools\u003c/strong\u003e: Applications where background tasks are secondary concerns, not architectural focal points.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNCronJob also supports instant jobs—one-time executions triggered programmatically, useful for workflows where scheduled tasks need manual activation or dependent tasks chain together.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#configuration-and-integration\" title=\"Configuration and Integration\"\u003eConfiguration and Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating NCronJob requires minimal setup. Install the NuGet package:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package NCronJob\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRegister jobs and schedules in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddNCronJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheCleanupJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 * * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Hourly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eHealthCheckJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 6 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Daily at 6 AM\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseNCronJobAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJobs implement \u003ccode\u003eIJob\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCacheCleanupJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eCacheCleanupJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003ecache\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecache\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRunAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoveExpiredEntriesAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNCronJob resolves dependencies from the DI container and injects them into jobs. The \u003ccode\u003eIJobExecutionContext\u003c/code\u003e provides metadata—execution time, parameters, cancellation tokens—enabling context-aware job logic.\u003c/p\u003e\n\u003cp\u003eInline jobs reduce boilerplate for simple tasks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProgram\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Heartbeat at {Time}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e},\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;*/5 * * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Every 5 minutes\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis fluent API mirrors Minimal APIs in ASP.NET Core, where route handlers are inline delegates with parameter injection. The consistency reduces cognitive load for developers familiar with modern .NET conventions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cron-expressions-and-scheduling-semantics\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#cron-expressions-and-scheduling-semantics\" title=\"Cron Expressions and Scheduling Semantics\"\u003eCron Expressions and Scheduling Semantics\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob uses standard five-field cron syntax:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e* * * * *\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ │ │ │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ │ │ └─── Day of week \u003cspan class=\"o\"\u003e(\u003c/span\u003e0-7, where \u003cspan class=\"m\"\u003e0\u003c/span\u003e and \u003cspan class=\"m\"\u003e7\u003c/span\u003e are Sunday\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ │ └───── Month \u003cspan class=\"o\"\u003e(\u003c/span\u003e1-12\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ │ └─────── Day of month \u003cspan class=\"o\"\u003e(\u003c/span\u003e1-31\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e│ └───────── Hour \u003cspan class=\"o\"\u003e(\u003c/span\u003e0-23\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└─────────── Minute \u003cspan class=\"o\"\u003e(\u003c/span\u003e0-59\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eExamples:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e0 * * * *\u003c/code\u003e: Every hour at minute 0.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e0 6 * * *\u003c/code\u003e: Daily at 6 AM.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e0 0 1 * *\u003c/code\u003e: Monthly on the 1st at midnight.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e*/15 * * * *\u003c/code\u003e: Every 15 minutes.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCron expressions are parsed at startup. Invalid expressions cause application startup failures, preventing silent misconfigurations. This fail-fast behavior aligns with modern cloud-native principles: catch configuration errors early, not in production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"timezone-aware-cron-scheduling\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#timezone-aware-cron-scheduling\" title=\"Timezone-Aware Cron Scheduling\"\u003eTimezone-Aware Cron Scheduling\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNCronJob supports timezone-aware scheduling:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTimeZoneConverter\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReportJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 9 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithTimeZone\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTZConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetTimeZoneInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;America/New_York\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis ensures jobs fire at correct local times regardless of server timezone settings—critical for multi-region deployments or applications serving global users.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-parameters-and-instant-execution\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#job-parameters-and-instant-execution\" title=\"Job Parameters and Instant Execution\"\u003eJob Parameters and Instant Execution\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJobs often need parameters—identifiers, configuration values, dynamic inputs. NCronJob passes parameters via the execution context:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDataImportJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 2 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithParameter\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;source\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;external-api\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDataImportJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eRunAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esource\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParameter\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eImportDataAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esource\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"triggering-jobs-programmatically\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#triggering-jobs-programmatically\" title=\"Triggering Jobs Programmatically\"\u003eTriggering Jobs Programmatically\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor workflows requiring manual job execution, NCronJob provides instant jobs via \u003ccode\u003eIInstantJobRegistry\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIInstantJobRegistry\u003c/span\u003e \u003cspan class=\"n\"\u003e_registry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIInstantJobRegistry\u003c/span\u003e \u003cspan class=\"n\"\u003eregistry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_registry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eregistry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eCompleteOrderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Process order logic...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_registry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunInstantJobAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eSendConfirmationJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eInstant jobs execute immediately on background threads, decoupling them from HTTP request lifetimes. This pattern suits scenarios where scheduled tasks need programmatic triggers—user-initiated reports, dependent workflows, or event-driven processing.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-dependencies-and-chaining\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#job-dependencies-and-chaining\" title=\"Job Dependencies and Chaining\"\u003eJob Dependencies and Chaining\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob supports job dependencies, enabling workflows where job B executes only after job A succeeds or fails:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eImportDataJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 2 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteWhen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTransformDataJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003efaulted\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eNotifyFailureJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen \u003ccode\u003eImportDataJob\u003c/code\u003e succeeds, \u003ccode\u003eTransformDataJob\u003c/code\u003e executes automatically. If it fails, \u003ccode\u003eNotifyFailureJob\u003c/code\u003e handles the error. This declarative approach simplifies common workflows without custom orchestration logic.\u003c/p\u003e\n\u003cp\u003eJob chaining also supports inline delegates:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessFileJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronExpression\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 3 * * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExecuteWhen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eINotificationService\u003c/span\u003e \u003cspan class=\"n\"\u003enotifier\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003enotifier\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;File processed successfully\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e})));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis fluency reduces boilerplate for simple dependent tasks, keeping configuration concise.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"startup-jobs-and-application-lifecycle\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#startup-jobs-and-application-lifecycle\" title=\"Startup Jobs and Application Lifecycle\"\u003eStartup Jobs and Application Lifecycle\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSome tasks must run immediately when the application starts—cache warming, database migrations, configuration validation. NCronJob supports startup jobs:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheWarmupJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRunAtStartup\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eUseNCronJobAsync()\u003c/code\u003e method executes startup jobs before the application begins serving requests, ensuring initialization completes synchronously. This prevents race conditions where HTTP requests arrive before background tasks finish preparing the system.\u003c/p\u003e\n\u003cp\u003eStartup jobs block application startup. If they fail, the application doesn\u0026rsquo;t start—matching fail-fast principles. For long-running initialization, consider splitting startup tasks into instant jobs triggered asynchronously after startup.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"stateless-design-and-cloud-native-fit\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#stateless-design-and-cloud-native-fit\" title=\"Stateless Design and Cloud-Native Fit\"\u003eStateless Design and Cloud-Native Fit\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob\u0026rsquo;s stateless design aligns with cloud-native architectures. Jobs run in-process without external dependencies, making deployments trivial: package the application, deploy it, and scheduling works. There\u0026rsquo;s no database to provision, no connection strings to manage, no external services to monitor.\u003c/p\u003e\n\u003cp\u003eThis simplicity shines in Kubernetes environments. Deploy multiple replicas of a service, and each runs its own scheduler. For idempotent tasks—cache cleanup, health checks—duplicate execution across replicas is harmless. For tasks requiring exactly-once execution, use external coordination mechanisms like distributed locks (e.g., \u003ccode\u003eDistributedLock\u003c/code\u003e NuGet package) or delegate scheduling to Kubernetes CronJobs.\u003c/p\u003e\n\u003cp\u003eNCronJob\u0026rsquo;s minimal footprint also reduces resource consumption. No database polling, no network I/O for coordination, no persistent storage writes. Jobs execute with negligible overhead, suitable for resource-constrained environments like edge devices or cost-sensitive cloud deployments.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"limitations-and-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#limitations-and-trade-offs\" title=\"Limitations and Trade-offs\"\u003eLimitations and Trade-offs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob\u0026rsquo;s simplicity imposes constraints:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo persistence\u003c/strong\u003e: Jobs don\u0026rsquo;t survive application restarts. If a scheduled task should have executed while the application was down, it won\u0026rsquo;t run upon restart. For workflows requiring guaranteed execution, Hangfire or TickerQ provide persistence.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo clustering\u003c/strong\u003e: Multiple instances execute jobs independently. Without external coordination, duplicate execution occurs. For tasks that must run exactly once across a cluster, use distributed locks or alternative schedulers.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo dashboard\u003c/strong\u003e: Observability relies on application logging and external monitoring tools. Teams needing real-time job visibility should consider Hangfire or TickerQ.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNo automatic retries\u003c/strong\u003e: Failed jobs don\u0026rsquo;t retry unless explicitly coded. Hangfire\u0026rsquo;s built-in retry policies and Quartz.NET\u0026rsquo;s misfire handling don\u0026rsquo;t exist in NCronJob.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"constraints-as-design-choices\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#constraints-as-design-choices\" title=\"Constraints As Design Choices\"\u003eConstraints As Design Choices\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThese limitations aren\u0026rsquo;t flaws—they\u0026rsquo;re intentional design choices favoring simplicity. For applications where jobs are transient, observability comes from logs, and horizontal scaling doesn\u0026rsquo;t require coordination, NCronJob\u0026rsquo;s constraints are acceptable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-ncronjob-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#when-ncronjob-fits\" title=\"When NCronJob Fits\"\u003eWhen NCronJob Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStateless deployments are required\u003c/strong\u003e: Containerized microservices, serverless functions, or ephemeral environments benefit from zero-dependency scheduling.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eJobs are idempotent or non-critical\u003c/strong\u003e: Tasks like cache warming, health checks, or metrics collection tolerate occasional duplication or missed executions.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity trumps features\u003c/strong\u003e: Teams that value minimal configuration and zero operational overhead over dashboards, clustering, or persistence.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNative .NET integration is prioritized\u003c/strong\u003e: Developers comfortable with \u003ccode\u003eIHostedService\u003c/code\u003e and modern .NET conventions find NCronJob\u0026rsquo;s API familiar and consistent.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch3 id=\"when-to-pick-a-different-scheduler\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#when-to-pick-a-different-scheduler\" title=\"When To Pick A Different Scheduler\"\u003eWhen To Pick A Different Scheduler\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNCronJob is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence is required\u003c/strong\u003e: User-initiated reports, financial workflows, or critical business processes demand database-backed job storage (see Hangfire or TickerQ).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eClustering is essential\u003c/strong\u003e: Distributed systems needing coordinated job execution across instances should use Quartz.NET or external coordination.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eObservability and dashboards matter\u003c/strong\u003e: Production systems requiring real-time job visibility benefit from Hangfire or TickerQ\u0026rsquo;s monitoring features.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNCronJob occupies the absolute minimalism position in the scheduling spectrum. It delivers cron-based scheduling with zero dependencies, ideal for cloud-native architectures prioritizing statelessness and simplicity.\u003c/p\u003e\n\u003cp\u003eConsider NCronJob if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application runs in Kubernetes, serverless, or containerized environments.\u003c/li\u003e\n\u003cli\u003eBackground tasks are transient and don\u0026rsquo;t require durability.\u003c/li\u003e\n\u003cli\u003eYou value zero operational overhead and native .NET integration.\u003c/li\u003e\n\u003cli\u003eJobs are idempotent or tolerate occasional duplication.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid NCronJob if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJobs must persist across restarts (see Hangfire or TickerQ).\u003c/li\u003e\n\u003cli\u003eHorizontal scaling requires coordinated execution (see Quartz.NET).\u003c/li\u003e\n\u003cli\u003eYou need built-in dashboards or retry policies (see Hangfire).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article explores TickerQ, a framework representing the modern generation of .NET job schedulers. It combines Entity Framework Core persistence with source generation for reflection-free execution, real-time dashboards with SignalR, and async-first design for cloud-native performance—bridging the gap between simplicity and enterprise-grade capabilities.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-09T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-5-ncronjob/","language":"en","summary":"NCronJob plugs into ASP.NET Core hosting to deliver zero-dependency, cron-based scheduling for microservices and containerized .NET deployments.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — NCronJob and Native Minimalism","url":"https://daily-devops.net/posts/dotnet-job-scheduling-5-ncronjob/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYou\u0026rsquo;re building an internal dashboard that aggregates metrics from multiple APIs. Every ten minutes, background tasks fetch data, transform it, and cache results. The dashboard serves a small team—ten users maximum—and runs on a single Azure App Service instance. Sure, if the app restarts at 2 AM during a platform update, you lose the scheduled job. But honestly? The next scheduled run happens at 2:10 AM anyway. The ten-minute gap doesn\u0026rsquo;t justify spinning up SQL Server just for job persistence.\u003c/p\u003e\n\u003cp\u003eYou need background scheduling, but not infrastructure overkill when failures are inconsequential and the application restarts cleanly.\u003c/p\u003e\n\u003cp\u003eCoravel targets this scenario: applications where simplicity, developer velocity, and rapid iteration outweigh the need for persistent job storage or distributed coordination. It provides fluent APIs for scheduling, queuing, caching, and even mailing—all without external dependencies. Jobs run in-memory, configuration is minimal, and integration with ASP.NET Core feels native. The trade-off: jobs don\u0026rsquo;t survive application restarts, and scaling horizontally requires external coordination.\u003c/p\u003e\n\u003cp\u003eFor small to medium applications—internal tools, MVPs, low-traffic SaaS products—Coravel reduces time from idea to deployment. For large-scale systems demanding persistence and clustering, the architectural constraints merit consideration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architecture-in-memory-simplicity\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#architecture-in-memory-simplicity\" title=\"Architecture: In-Memory Simplicity\"\u003eArchitecture: In-Memory Simplicity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s architecture centers on in-memory task scheduling. It uses \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e under the hood, wrapped in a fluent API that hides timer management complexity. Jobs are defined as classes implementing \u003ccode\u003eIInvocable\u003c/code\u003e or as inline lambda expressions. The scheduler maintains a list of scheduled tasks and fires them based on configured intervals or cron expressions.\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s no database, no message queue, no external storage. When you schedule a job, Coravel stores its definition in memory. When the application restarts, scheduled jobs disappear. This design minimizes operational overhead but requires accepting transience: if persistence matters, Coravel isn\u0026rsquo;t the solution.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-comes-bundled-in-the-box\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#what-comes-bundled-in-the-box\" title=\"What Comes Bundled In The Box\"\u003eWhat Comes Bundled In The Box\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCoravel provides several integrated features beyond scheduling:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTask scheduling\u003c/strong\u003e: Cron-based or interval-based job execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eQueuing\u003c/strong\u003e: Offload work to background queues processed asynchronously.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCaching\u003c/strong\u003e: In-memory caching with expiration and eviction policies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMailing\u003c/strong\u003e: SMTP-based email sending with Razor template support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEvent broadcasting\u003c/strong\u003e: Loosely coupled event-driven architectures.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese features share a common design philosophy: convention over configuration. Coravel assumes sensible defaults, reduces boilerplate, and optimizes for developer ergonomics. Teams that value rapid prototyping and reduced operational complexity benefit from this approach.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#configuration-and-integration\" title=\"Configuration and Integration\"\u003eConfiguration and Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating Coravel into an ASP.NET Core application requires minimal setup. Install the NuGet package:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Coravel\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRegister services and configure scheduling in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTransient\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDataRefreshTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseScheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eDataRefreshTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;().\u003c/span\u003e\u003cspan class=\"n\"\u003eEveryTenMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration schedules \u003ccode\u003eDataRefreshTask\u003c/code\u003e to execute every ten minutes. The task implements \u003ccode\u003eIInvocable\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eDataRefreshTask\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIInvocable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIApiClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_apiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003e_cacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eDataRefreshTask\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIApiClient\u003c/span\u003e \u003cspan class=\"n\"\u003eapiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eICacheService\u003c/span\u003e \u003cspan class=\"n\"\u003ecacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_apiClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eapiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cacheService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvoke\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_apiClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFetchMetricsAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_cacheService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;metrics\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCoravel resolves \u003ccode\u003eDataRefreshTask\u003c/code\u003e from the DI container, injecting dependencies automatically. This feels consistent with ASP.NET Core\u0026rsquo;s conventions—no special registration or service location patterns required.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"scheduling-inline-lambdas\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#scheduling-inline-lambdas\" title=\"Scheduling Inline Lambdas\"\u003eScheduling Inline Lambdas\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAlternatively, schedule inline tasks for quick prototyping:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003escope\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateScope\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003escope\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServiceProvider\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetRequiredService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProgram\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Health check executed at {Time}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEveryFiveMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eInline tasks bypass the need for separate classes, accelerating development when job logic is simple.\u003c/p\u003e\n\u003cp\u003eCoravel\u0026rsquo;s fluent API supports various scheduling patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eEmailDigestTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDaily\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 8:00 AM\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWeekday\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReportTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCron\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 0 1 * *\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Monthly at midnight on the 1st\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe API reads like natural language, reducing cognitive load compared to raw cron syntax.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"queuing-for-asynchronous-work\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#queuing-for-asynchronous-work\" title=\"Queuing for Asynchronous Work\"\u003eQueuing for Asynchronous Work\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s queuing feature offloads time-consuming operations from HTTP request threads. Unlike scheduling, which triggers jobs at specific times, queuing executes jobs as soon as workers are available.\u003c/p\u003e\n\u003cp\u003eConfigure queuing:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddQueue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEnqueue jobs from controllers or services:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIQueue\u003c/span\u003e \u003cspan class=\"n\"\u003e_queue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderController\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIQueue\u003c/span\u003e \u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_queue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003equeue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpPost]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIActionResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_queue\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQueueInvocableWithPayload\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessOrderTask\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eAccepted\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe task receives the payload:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProcessOrderTask\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIInvocable\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIInvocableWithPayload\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003ePayload\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvoke\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Process order asynchronously\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ePayload\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-cost-of-an-in-memory-queue\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#the-cost-of-an-in-memory-queue\" title=\"The Cost Of An In-Memory Queue\"\u003eThe Cost Of An In-Memory Queue\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eQueued jobs execute on background threads managed by Coravel. The queue is in-memory—jobs don\u0026rsquo;t persist if the application restarts. For critical workflows requiring durability, Hangfire\u0026rsquo;s persistent queues or message brokers like RabbitMQ are necessary.\u003c/p\u003e\n\u003cp\u003eCoravel\u0026rsquo;s queue simplicity suits scenarios where occasional job loss is acceptable—cache warming, non-critical notifications, internal tools. For user-facing workflows like payment processing, persistence is non-negotiable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"caching-and-mailing-integrated-conveniences\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#caching-and-mailing-integrated-conveniences\" title=\"Caching and Mailing: Integrated Conveniences\"\u003eCaching and Mailing: Integrated Conveniences\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel bundles caching and mailing features that reduce dependency on third-party libraries.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCaching\u003c/strong\u003e wraps \u003ccode\u003eIMemoryCache\u003c/code\u003e with a fluent API:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCache\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_cache\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemember\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user-123\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eFetchUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e123\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eRemember\u003c/code\u003e method fetches data from cache if available, otherwise invokes the factory function, caches the result, and returns it. This pattern reduces boilerplate compared to manual cache checks.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"sending-mail-with-razor-templates\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#sending-mail-with-razor-templates\" title=\"Sending Mail With Razor Templates\"\u003eSending Mail With Razor Templates\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eMailing\u003c/strong\u003e supports SMTP and in-memory drivers:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddMailer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// appsettings.json\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"s\"\u003e\u0026#34;Coravel\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;Mail\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Driver\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;SMTP\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Host\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;smtp.example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Port\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e587\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Username\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"s\"\u003e\u0026#34;Password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;pass\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSend emails using Razor templates:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eWelcomeEmail\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eMailable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e \u003cspan class=\"n\"\u003e_user\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eWelcomeEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_user\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003eoverride\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_user\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFrom\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;noreply@example.com\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSubject\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Welcome!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eView\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;~/Views/Emails/Welcome.cshtml\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_user\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Send email\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_mailer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eWelcomeEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCoravel\u0026rsquo;s mailing abstraction simplifies email workflows common in small applications—welcome emails, password resets, notifications. For high-volume transactional email requiring templates, deliverability tracking, and vendor integrations, specialized services like SendGrid or Mailgun are more appropriate.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"event-broadcasting-for-loose-coupling\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#event-broadcasting-for-loose-coupling\" title=\"Event Broadcasting for Loose Coupling\"\u003eEvent Broadcasting for Loose Coupling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s event system decouples components via publish-subscribe patterns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddEvents\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Define event\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderPlacedEvent\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003eget\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Define listener\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eSendOrderConfirmationListener\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eHandleAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e \u003cspan class=\"n\"\u003e@event\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eSendConfirmationEmailAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e@event\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Register listener\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTransient\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;,\u003c/span\u003e \u003cspan class=\"n\"\u003eSendOrderConfirmationListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Broadcast event\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_dispatcher\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBroadcast\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderPlacedEvent\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e123\u003c/span\u003e \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eListeners execute synchronously unless queued via \u003ccode\u003eQueueBroadcast\u003c/code\u003e, which processes them asynchronously. This pattern suits workflows where side effects—logging, notifications, analytics—shouldn\u0026rsquo;t block primary operations.\u003c/p\u003e\n\u003cp\u003eCoravel\u0026rsquo;s event system is in-process. For distributed event-driven architectures spanning multiple services, message brokers like Azure Service Bus or RabbitMQ provide guarantees Coravel doesn\u0026rsquo;t: durability, at-least-once delivery, and cross-service communication.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"developer-experience-and-rapid-prototyping\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#developer-experience-and-rapid-prototyping\" title=\"Developer Experience and Rapid Prototyping\"\u003eDeveloper Experience and Rapid Prototyping\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s primary strength is developer experience. Its fluent APIs, convention-driven design, and zero external dependencies accelerate development cycles. Teams building MVPs, internal tools, or low-traffic applications spend less time configuring infrastructure and more time delivering features.\u003c/p\u003e\n\u003cp\u003eConsider a startup building a SaaS product. The initial version supports a few dozen users and runs on a single server. Background tasks refresh caches, send emails, and clean up temporary files. Coravel handles these needs without requiring database setup, message queue configuration, or understanding distributed systems concepts. As the product scales, the team can migrate to Hangfire or Quartz.NET—but during the critical early phase, Coravel removes friction.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-solo-developers-reach-for-coravel\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#why-solo-developers-reach-for-coravel\" title=\"Why Solo Developers Reach For Coravel\"\u003eWhy Solo Developers Reach For Coravel\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCoravel also appeals to solo developers or small teams lacking DevOps expertise. Deploying Hangfire requires provisioning SQL Server, managing connection strings, and monitoring database health. Coravel deploys with the application—no external dependencies, no infrastructure configuration.\u003c/p\u003e\n\u003cp\u003eThe trade-offs are clear: jobs don\u0026rsquo;t persist, scaling horizontally requires external coordination, and observability relies on application logging. For systems where these limitations are acceptable, Coravel\u0026rsquo;s simplicity is a competitive advantage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-coravel-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#when-coravel-fits\" title=\"When Coravel Fits\"\u003eWhen Coravel Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity and velocity are priorities\u003c/strong\u003e: Teams that value rapid iteration over operational robustness benefit from Coravel\u0026rsquo;s zero-configuration approach.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eJob persistence is unnecessary\u003c/strong\u003e: Applications where background tasks are ephemeral—cache refreshes, health checks, non-critical notifications—don\u0026rsquo;t need database-backed durability.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSingle-instance deployments suffice\u003c/strong\u003e: Applications running on a single server or containerized environment without horizontal scaling requirements fit Coravel\u0026rsquo;s in-memory design.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eIntegrated features reduce dependencies\u003c/strong\u003e: Teams that need scheduling, queuing, caching, and mailing without pulling in multiple libraries appreciate Coravel\u0026rsquo;s bundled approach.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eCoravel is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence is non-negotiable\u003c/strong\u003e: User-initiated reports, financial transactions, or workflows requiring guaranteed execution demand database-backed storage (see Hangfire or TickerQ).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHorizontal scaling is planned\u003c/strong\u003e: Running multiple instances without job duplication requires external coordination mechanisms Coravel doesn\u0026rsquo;t provide.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHigh observability is critical\u003c/strong\u003e: Production systems needing detailed job execution history, failure analysis, and dashboards benefit from Hangfire or Quartz.NET.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-simplicity-and-limitations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#operational-simplicity-and-limitations\" title=\"Operational Simplicity and Limitations\"\u003eOperational Simplicity and Limitations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel\u0026rsquo;s operational footprint is minimal. It runs in-process, consumes minimal memory, and requires no external services. Deployments are straightforward: push the application, and scheduling works. There\u0026rsquo;s no database schema to migrate, no message queue to monitor, no clustering configuration to tune.\u003c/p\u003e\n\u003cp\u003eThe limitations stem from this simplicity. Jobs vanish on restart. If your application crashes mid-execution, queued work is lost. Horizontal scaling without external coordination leads to duplicate job execution—multiple instances schedule the same tasks independently.\u003c/p\u003e\n\u003cp\u003eFor applications where these constraints are acceptable, Coravel\u0026rsquo;s operational simplicity is liberating. Teams avoid the overhead of managing persistent storage, monitoring database health, or troubleshooting distributed coordination failures. Background processing becomes invisible infrastructure rather than a system to operate.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCoravel occupies the simplicity-first position in the scheduling spectrum. It trades persistence and clustering for developer velocity and zero dependencies.\u003c/p\u003e\n\u003cp\u003eConsider Coravel if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application runs on a single instance without horizontal scaling plans.\u003c/li\u003e\n\u003cli\u003eBackground jobs are transient and don\u0026rsquo;t require durability.\u003c/li\u003e\n\u003cli\u003eDeveloper velocity and minimal configuration trump advanced features.\u003c/li\u003e\n\u003cli\u003eYou need integrated queuing, caching, or mailing without managing multiple libraries.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid Coravel if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eJobs must survive application restarts (see Hangfire or TickerQ).\u003c/li\u003e\n\u003cli\u003eHorizontal scaling requires coordinated job execution (see Quartz.NET).\u003c/li\u003e\n\u003cli\u003eDetailed observability and dashboards are critical (see Hangfire).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article examines NCronJob, a framework that emphasizes minimalism even further. Where Coravel bundles features like caching and mailing, NCronJob focuses exclusively on scheduling with direct integration into ASP.NET Core\u0026rsquo;s hosting model, appealing to teams seeking the absolute minimum infrastructure overhead.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-04T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-4-coravel/","language":"en","summary":"How Coravel delivers lightweight, convention-driven scheduling without external dependencies, accelerating development for small to medium applications.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Coravel and Fluent Simplicity","url":"https://daily-devops.net/posts/dotnet-job-scheduling-4-coravel/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eYour financial platform processes millions of transactions daily. At midnight, the system must calculate interest for every account, generate regulatory reports, and trigger fraud detection sweeps. These tasks cannot overlap—interest calculation must complete before reporting begins. Some jobs repeat hourly, others run on the last business day of each month, skipping holidays. A single scheduler instance cannot handle the throughput, but multiple instances must coordinate to prevent duplicate execution.\u003c/p\u003e\n\u003cp\u003eThis is Quartz.NET\u0026rsquo;s domain: enterprise-grade job scheduling where complexity, throughput, and reliability intersect. Quartz.NET targets systems with demanding scheduling semantics—job calendars that respect business rules, priority-based execution, clustering across datacenters, and integration with external monitoring infrastructure.\u003c/p\u003e\n\u003cp\u003eThe trade-off: operational complexity. Quartz.NET requires careful configuration, understanding of its architectural patterns, and infrastructure to support distributed coordination. For systems where scheduling is a first-class concern—ETL pipelines, financial batch processing, multi-tenant SaaS platforms—this investment pays dividends. For applications where background jobs are secondary concerns, the complexity may outweigh the benefits.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architecture-jobs-triggers-and-the-scheduler\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#architecture-jobs-triggers-and-the-scheduler\" title=\"Architecture: Jobs, Triggers, and the Scheduler\"\u003eArchitecture: Jobs, Triggers, and the Scheduler\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s architecture decomposes scheduling into three core abstractions: jobs, triggers, and the scheduler.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eJobs\u003c/strong\u003e define what to execute. They implement \u003ccode\u003eIJob\u003c/code\u003e, a single-method interface:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eInterestCalculationJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobDetail\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobDataMap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountService\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateInterestAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJobs are stateless. The scheduler instantiates them on demand, injects dependencies via job data maps or DI containers, and discards them after execution. This statelessness enables clustering: any scheduler instance can execute any job without requiring sticky sessions or shared state.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"trigger-types-and-their-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#trigger-types-and-their-trade-offs\" title=\"Trigger Types And Their Trade-offs\"\u003eTrigger Types And Their Trade-offs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eTriggers\u003c/strong\u003e define when jobs execute. Quartz.NET supports several trigger types:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSimple triggers\u003c/strong\u003e: Execute once after a delay or repeat at fixed intervals.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCron triggers\u003c/strong\u003e: Use cron expressions for complex schedules like \u0026ldquo;every Monday at 9 AM\u0026rdquo; or \u0026ldquo;the last Friday of each month.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCalendar interval triggers\u003c/strong\u003e: Repeat at intervals respecting business calendars—every month, every quarter, skipping holidays.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDaily time interval triggers\u003c/strong\u003e: Run between specific hours on selected days—useful for jobs that should only execute during business hours.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTriggers can include misfire policies—rules for handling missed executions when the scheduler is offline or overloaded. For example, a trigger might specify \u0026ldquo;execute immediately upon recovery\u0026rdquo; or \u0026ldquo;skip missed executions and wait for the next scheduled time.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-the-scheduler-claims-triggers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#how-the-scheduler-claims-triggers\" title=\"How The Scheduler Claims Triggers\"\u003eHow The Scheduler Claims Triggers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eThe scheduler\u003c/strong\u003e coordinates jobs and triggers. It stores definitions in persistent storage (SQL Server, PostgreSQL, Oracle, or in-memory), polls for triggers whose fire times have arrived, claims them atomically to prevent duplicate execution, and dispatches jobs to worker threads.\u003c/p\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s scheduler supports clustering: multiple instances share the same database, coordinating via database locks. When a trigger fires, one instance claims it using optimistic locking (\u003ccode\u003eUPDATE ... WHERE locked_by IS NULL\u003c/code\u003e). If the instance crashes mid-execution, another instance detects the orphaned job and recovers it based on the trigger\u0026rsquo;s misfire policy.\u003c/p\u003e\n\u003cp\u003eThis design scales horizontally. Add more scheduler instances to increase throughput. Each instance competes for jobs via database coordination, distributing workload automatically without manual partitioning or configuration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration-with-aspnet-core\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#configuration-and-integration-with-aspnet-core\" title=\"Configuration and Integration with ASP.NET Core\"\u003eConfiguration and Integration with ASP.NET Core\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating Quartz.NET into an ASP.NET Core application involves configuring storage, defining jobs and triggers, and starting the scheduler.\u003c/p\u003e\n\u003cp\u003eFirst, install the NuGet packages:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz.Extensions.Hosting\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz.Serialization.Json\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Quartz.Plugins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSecond, configure the scheduler in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddQuartz\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseMicrosoftDependencyInjectionJobFactory\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePersistentStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Host=localhost;Database=quartz;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseJsonSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejobKey\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eJobKey\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;InterestCalculation\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInterestCalculationJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eopts\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eopts\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejobKey\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddTrigger\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eopts\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eopts\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eForJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejobKey\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;InterestCalculation-trigger\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 0 0 * * ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Daily at midnight\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddQuartzHostedService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWaitForJobsToComplete\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration uses PostgreSQL for storage, schedules a job to run daily at midnight, and ensures the scheduler waits for jobs to complete during application shutdown.\u003c/p\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s hosted service integration leverages ASP.NET Core\u0026rsquo;s \u003ccode\u003eIHostedService\u003c/code\u003e, starting and stopping the scheduler alongside the application lifecycle. The \u003ccode\u003eWaitForJobsToComplete\u003c/code\u003e option ensures graceful shutdowns: the scheduler finishes executing jobs before the application terminates, preventing interrupted workflows.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"injecting-services-into-jobs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#injecting-services-into-jobs\" title=\"Injecting Services Into Jobs\"\u003eInjecting Services Into Jobs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eJobs receive dependencies via constructor injection when using \u003ccode\u003eUseMicrosoftDependencyInjectionJobFactory()\u003c/code\u003e. This eliminates the need for manual service resolution:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eReportGenerationJob\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJob\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eReportGenerationJob\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportService\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ereportService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_reportService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGenerateMonthlyReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe scheduler resolves \u003ccode\u003eIReportService\u003c/code\u003e from the DI container and injects it into the job. This integration feels native to ASP.NET Core, reducing boilerplate compared to manual service location patterns.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"advanced-scheduling-calendars-and-misfires\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#advanced-scheduling-calendars-and-misfires\" title=\"Advanced Scheduling: Calendars and Misfires\"\u003eAdvanced Scheduling: Calendars and Misfires\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s calendar support enables business-aware scheduling. Calendars exclude specific dates—holidays, maintenance windows—from trigger schedules. For example, a job scheduled to run daily except holidays:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eholidays\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eHolidayCalendar\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eholidays\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddExcludedDate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2025\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e25\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Christmas\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eholidays\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddExcludedDate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2025\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// New Year\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003escheduler\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddCalendar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;US-Holidays\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eholidays\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ereplace\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eupdateTriggers\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etrigger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTriggerBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DailyProcessing\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 9 * * ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInTimeZone\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeZoneInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindSystemTimeZoneById\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Eastern Standard Time\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eModifiedByCalendar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;US-Holidays\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis trigger fires at 9 AM daily, Eastern Time, but skips days marked in the \u003ccode\u003eUS-Holidays\u003c/code\u003e calendar. Quartz.NET evaluates calendars during trigger computation, deferring execution to the next valid day.\u003c/p\u003e\n\u003cp\u003eCalendar types include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHolidayCalendar\u003c/strong\u003e: Exclude specific dates.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCronCalendar\u003c/strong\u003e: Exclude dates matching a cron expression.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDailyCalendar\u003c/strong\u003e: Exclude time ranges (e.g., \u0026ldquo;skip execution between 2 AM and 6 AM\u0026rdquo;).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonthlyCalendar\u003c/strong\u003e: Exclude specific days of the month.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCombining calendars creates sophisticated rules. A trigger might exclude weekends, holidays, and the first Monday of each month—all declaratively, without custom logic.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"choosing-a-misfire-policy\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#choosing-a-misfire-policy\" title=\"Choosing A Misfire Policy\"\u003eChoosing A Misfire Policy\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eMisfire policies\u003c/strong\u003e handle execution gaps when the scheduler is offline or overloaded. If a job scheduled for 2 AM doesn\u0026rsquo;t execute until 3 AM because the scheduler was down, the misfire policy determines behavior:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDoNothing\u003c/strong\u003e: Skip the missed execution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFireNow\u003c/strong\u003e: Execute immediately upon recovery.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFireAndProceed\u003c/strong\u003e: Execute missed runs, then continue with the normal schedule.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFireOnceNow\u003c/strong\u003e: Execute once immediately, then resume the schedule.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eConfigure misfire policies per trigger:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003etrigger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTriggerBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;DataImport\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithCronSchedule\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0 0 2 * * ?\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithMisfireHandlingInstructionFireAndProceed\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis trigger ensures missed nightly imports execute upon scheduler recovery, preventing data gaps.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"clustering-and-distributed-coordination\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#clustering-and-distributed-coordination\" title=\"Clustering and Distributed Coordination\"\u003eClustering and Distributed Coordination\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s clustering enables horizontal scaling and high availability. Multiple scheduler instances share a database, coordinating via optimistic locking to prevent duplicate job execution. I\u0026rsquo;ve run three-node Quartz.NET clusters processing 15,000+ jobs daily, and the coordination works—but you need to understand what\u0026rsquo;s happening under the hood.\u003c/p\u003e\n\u003cp\u003eWhen a trigger fires, the scheduler that claims it updates a database row with its instance ID:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eUPDATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eqrtz_triggers\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eSET\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003estate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;ACQUIRED\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003einstance_name\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;scheduler-01\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eWHERE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etrigger_name\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;DataImport\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eAND\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003estate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;WAITING\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOnly one scheduler succeeds. Others skip the trigger and poll for the next available job. This database-based coordination avoids requiring external coordination services like ZooKeeper or Consul.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"recovering-orphaned-jobs-after-a-crash\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#recovering-orphaned-jobs-after-a-crash\" title=\"Recovering Orphaned Jobs After A Crash\"\u003eRecovering Orphaned Jobs After A Crash\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf a scheduler crashes mid-execution, orphaned jobs remain in the \u003ccode\u003eACQUIRED\u003c/code\u003e state. A recovery thread detects these jobs (based on a timeout threshold) and resets them to \u003ccode\u003eWAITING\u003c/code\u003e, allowing another scheduler to claim them. The interval and timeout are configurable:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePersistentStore\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsePostgres\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseClusteredMode\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePerformSchemaValidation\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eClustering introduces latency: each scheduler instance polls the database for triggers, typically every few seconds. For high-throughput scenarios, this creates database load proportional to instance count. Tuning polling intervals balances responsiveness and database overhead.\u003c/p\u003e\n\u003cp\u003eQuartz.NET also supports \u003cstrong\u003ejob persistence without clustering\u003c/strong\u003e. Single-instance deployments benefit from persistent storage (jobs survive restarts) without coordination overhead. This mode suits applications where high availability isn\u0026rsquo;t critical but durability matters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"job-data-maps-and-parameterization\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#job-data-maps-and-parameterization\" title=\"Job Data Maps and Parameterization\"\u003eJob Data Maps and Parameterization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJobs often require parameters—account IDs, file paths, configuration values. Quartz.NET uses \u003cstrong\u003ejob data maps\u003c/strong\u003e to pass data:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejobData\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eJobDataMap\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;accountId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e12345\u003c/span\u003e \u003cspan class=\"p\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;reportType\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;monthly\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ejob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eJobBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eReportJob\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWithIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Report-12345\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUsingJobData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ejobData\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJobs retrieve parameters from the execution context:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eExecute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eaccountId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMergedJobDataMap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetInt\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;accountId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ereportType\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMergedJobDataMap\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;reportType\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateReportAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eaccountId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ereportType\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eQuartz.NET serializes job data maps to the database using JSON. Complex types—custom classes, collections—are supported, but large payloads impact performance. For heavyweight data, pass identifiers (e.g., database primary keys) and fetch the data within the job.\u003c/p\u003e\n\u003cp\u003eTriggers can also carry data maps, which merge with job data maps during execution. This enables per-trigger customization: a single job definition with multiple triggers, each passing different parameters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"monitoring-plugins-and-extensibility\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#monitoring-plugins-and-extensibility\" title=\"Monitoring, Plugins, and Extensibility\"\u003eMonitoring, Plugins, and Extensibility\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET provides listeners for observing job lifecycle events:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eJobExecutionListener\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIJobListener\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;JobExecutionListener\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eJobWasExecuted\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIJobExecutionContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eJobExecutionException\u003c/span\u003e\u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eduration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobRunTime\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Job {JobKey} executed in {Duration}ms\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJobDetail\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eKey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eduration\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalMilliseconds\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompletedTask\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Other lifecycle methods...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRegister listeners during configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddJobListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eJobExecutionListener\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eListeners integrate with telemetry systems—Application Insights, Prometheus, Datadog—exporting metrics like job execution time, failure rates, and queue depths. This observability is critical for production systems where job health impacts business operations.\u003c/p\u003e\n\u003cp\u003eQuartz.NET includes plugins for common scenarios:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eXMLSchedulingDataProcessorPlugin\u003c/strong\u003e: Load job definitions from XML files, enabling configuration-driven scheduling.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLoggingTriggerHistoryPlugin\u003c/strong\u003e: Records trigger fire history to logs for audit trails.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInterruptMonitorPlugin\u003c/strong\u003e: Monitors job interruptions and logs them for debugging.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003ePlugins integrate via configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddXMLSchedulingDataProcessorPlugin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eplugin\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eplugin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFiles\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;~/quartz_jobs.xml\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eplugin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eScanInterval\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromSeconds\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e30\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis plugin watches an XML file and dynamically updates job definitions without application restarts—useful for operational teams adjusting schedules without developer intervention.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-quartznet-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#when-quartznet-fits\" title=\"When Quartz.NET Fits\"\u003eWhen Quartz.NET Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET excels when:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eComplex scheduling is essential\u003c/strong\u003e: Job calendars, business day logic, misfire policies, and priority-based execution are first-class requirements.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHigh throughput demands clustering\u003c/strong\u003e: Thousands or tens of thousands of jobs per minute justify distributed coordination and horizontal scaling.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eObservability and auditability matter\u003c/strong\u003e: Enterprises needing compliance, audit trails, and detailed execution history benefit from Quartz.NET\u0026rsquo;s persistence and plugin ecosystem.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eMulti-tenancy or geo-distribution\u003c/strong\u003e: Systems spanning multiple datacenters or customer tenants require flexible storage and isolation, which Quartz.NET\u0026rsquo;s architecture supports.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eQuartz.NET is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSimplicity is paramount\u003c/strong\u003e: Teams seeking minimal configuration overhead should consider Hangfire, Coravel, or NCronJob.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStateless deployments are preferred\u003c/strong\u003e: While Quartz.NET supports in-memory storage, clustering requires a database. Fully stateless architectures might prefer external message brokers or in-memory frameworks.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eThroughput is modest\u003c/strong\u003e: If job volumes are hundreds per minute, not thousands, Quartz.NET\u0026rsquo;s complexity may outweigh its benefits. Hangfire delivers adequate performance with less operational overhead.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-complexity-and-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#operational-complexity-and-trade-offs\" title=\"Operational Complexity and Trade-offs\"\u003eOperational Complexity and Trade-offs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET\u0026rsquo;s power comes with operational demands. Teams must provision and maintain database infrastructure, configure clustering correctly, monitor database performance under polling load, and tune misfire policies based on workload characteristics.\u003c/p\u003e\n\u003cp\u003eThe learning curve is steeper than simpler frameworks. Quartz.NET\u0026rsquo;s abstractions—jobs, triggers, calendars, listeners—require understanding before effective use. Misconfigured misfire policies can cause execution storms (hundreds of missed jobs firing simultaneously). Incorrect clustering settings can lead to duplicate execution or job starvation.\u003c/p\u003e\n\u003cp\u003eHowever, for systems where scheduling is critical, this complexity is justified. Quartz.NET\u0026rsquo;s reliability, flexibility, and scalability enable architectures that simpler frameworks cannot support. Financial platforms, healthcare systems, and enterprise ETL pipelines rely on Quartz.NET for workloads where failures have business consequences.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eQuartz.NET occupies the enterprise end of the scheduling spectrum. It provides advanced semantics, clustering, and observability at the cost of operational complexity.\u003c/p\u003e\n\u003cp\u003eConsider Quartz.NET if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour system requires complex scheduling—calendars, misfires, priorities.\u003c/li\u003e\n\u003cli\u003eJob volumes justify horizontal scaling across multiple instances.\u003c/li\u003e\n\u003cli\u003eObservability, auditing, and compliance are critical.\u003c/li\u003e\n\u003cli\u003eYou need fine-grained control over execution policies and error handling.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid Quartz.NET if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application needs simple, lightweight scheduling (see NCronJob or Coravel).\u003c/li\u003e\n\u003cli\u003ePersistence suffices without clustering (see Hangfire).\u003c/li\u003e\n\u003cli\u003eDeveloper velocity and minimal configuration are priorities over advanced features.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article explores Coravel, a framework that prioritizes simplicity and developer convenience. Where Quartz.NET offers enterprise control, Coravel provides fluent APIs, zero infrastructure requirements, and rapid integration for small to medium applications.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-02T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-3-quartznet/","language":"en","summary":"Quartz.NET delivers enterprise scheduling with clustering, advanced triggers, job calendars, and multi-datacenter coordination for high-volume workloads.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Quartz.NET for Enterprise Scale","url":"https://daily-devops.net/posts/dotnet-job-scheduling-3-quartznet/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA user uploads a 200 MB video to your platform at 3:14 PM. Transcoding it into multiple formats—1080p, 720p, mobile—takes twelve minutes on average, sometimes longer. Keeping the HTTP request open that long? Unacceptable. But here\u0026rsquo;s the problem: during our Tuesday maintenance window last month, we restarted the app servers, and boom—87 video processing jobs vanished into thin air. Users got \u0026ldquo;upload successful\u0026rdquo; messages, but their videos never appeared. Not ideal when you\u0026rsquo;re charging for the service.\u003c/p\u003e\n\u003cp\u003eYou need persistence: the ability to store job definitions in a database, detach them from the request lifecycle, and guarantee execution even when infrastructure hiccups.\u003c/p\u003e\n\u003cp\u003eHangfire solves this by turning background jobs into first-class database records. When you enqueue a job, Hangfire serializes the method invocation—class name, method signature, parameters—and persists it to SQL Server, PostgreSQL, or Redis. Worker threads poll the storage, claim jobs, execute them, and record outcomes. If a worker crashes mid-execution, another worker picks up the job and retries it based on configurable policies. If the entire application restarts, queued jobs remain intact, waiting for workers to resume processing.\u003c/p\u003e\n\u003cp\u003eThis architecture makes Hangfire particularly suited for web applications where background work must survive deployments, process restarts, or transient failures. The trade-off: you need a database. For teams already running SQL Server or PostgreSQL, this is minimal overhead. For environments preferring stateless components, the infrastructure requirement merits consideration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"core-architecture-storage-workers-and-coordination\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#core-architecture-storage-workers-and-coordination\" title=\"Core Architecture: Storage, Workers, and Coordination\"\u003eCore Architecture: Storage, Workers, and Coordination\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire\u0026rsquo;s design centers on three components: the storage backend, the job server (workers), and the client API that enqueues jobs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStorage\u003c/strong\u003e holds job definitions, execution history, and metadata. Hangfire serializes method calls—including parameter values—as JSON and stores them in tables like \u003ccode\u003eHangFire.Job\u003c/code\u003e, \u003ccode\u003eHangFire.State\u003c/code\u003e, and \u003ccode\u003eHangFire.JobQueue\u003c/code\u003e. When a job is enqueued, a record appears in the database. When a worker processes it, the state transitions from \u003ccode\u003eEnqueued\u003c/code\u003e to \u003ccode\u003eProcessing\u003c/code\u003e to \u003ccode\u003eSucceeded\u003c/code\u003e or \u003ccode\u003eFailed\u003c/code\u003e. This persistence is what differentiates Hangfire from in-memory schedulers: jobs are durable, observable, and recoverable.\u003c/p\u003e\n\u003cp\u003eSupported storage backends include SQL Server (the default), PostgreSQL, MySQL, MongoDB, and Redis. SQL-based backends offer strong consistency and integrate seamlessly with existing relational infrastructure. Redis provides lower latency for high-throughput scenarios where job volumes exceed thousands per minute. Choosing a backend depends on your existing infrastructure and performance requirements—SQL Server for most .NET shops, Redis for systems already using it for caching or session state.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-workers-claim-jobs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#how-workers-claim-jobs\" title=\"How Workers Claim Jobs\"\u003eHow Workers Claim Jobs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eWorkers\u003c/strong\u003e execute jobs. Each Hangfire server instance starts dedicated background threads—not the ASP.NET Core thread pool—that poll the storage for \u003ccode\u003eEnqueued\u003c/code\u003e jobs. Polling uses database-specific mechanisms: SQL Server leverages \u003ccode\u003eUPDLOCK\u003c/code\u003e and \u003ccode\u003eREADPAST\u003c/code\u003e hints to claim jobs atomically, ensuring only one worker processes each job even when multiple servers run concurrently. Workers fetch jobs, deserialize method calls, invoke them using reflection, and update job states in the database.\u003c/p\u003e\n\u003cp\u003eThe number of worker threads is configurable. A single-instance application might run five workers; a scaled-out deployment with three servers might run fifteen total workers (five per server). More workers increase throughput but consume more database connections and CPU. Tuning depends on job execution time: CPU-bound jobs benefit from fewer workers matching CPU core counts, while I/O-bound jobs can support more workers since threads spend time waiting on external resources.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"enqueuing-from-the-client-api\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#enqueuing-from-the-client-api\" title=\"Enqueuing From the Client API\"\u003eEnqueuing From the Client API\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eClients\u003c/strong\u003e enqueue jobs via a simple API. \u003ccode\u003eBackgroundJob.Enqueue(() =\u0026gt; Console.WriteLine(\u0026quot;Hello\u0026quot;))\u003c/code\u003e serializes the method call and inserts it into the database. The calling thread returns immediately; the work happens asynchronously on a worker thread. This decoupling is essential for web applications: controllers enqueue jobs in milliseconds and respond to users, while workers process jobs in the background without blocking HTTP requests.\u003c/p\u003e\n\u003cp\u003eHangfire also supports delayed jobs (scheduled to run after a time interval), recurring jobs (executed on a cron schedule), and continuations (jobs that run after a parent job succeeds). Each pattern maps to database records with corresponding state transitions, enabling rich workflows without custom orchestration code.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-and-integration\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#configuration-and-integration\" title=\"Configuration and Integration\"\u003eConfiguration and Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntegrating Hangfire into an ASP.NET Core application requires three steps: configuring storage, starting the server, and optionally enabling the dashboard.\u003c/p\u003e\n\u003cp\u003eFirst, install the NuGet package. For SQL Server:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Hangfire.AspNetCore\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Hangfire.SqlServer\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSecond, configure storage and start the server in \u003ccode\u003eProgram.cs\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfire\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetDataCompatibilityLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCompatibilityLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eVersion_180\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseSimpleAssemblyNameTypeSerializer\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseRecommendedSerializerSettings\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseSqlServerStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Server=.;Database=HangfireDB;Integrated Security=True;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfireServer\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseHangfireDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis configuration connects to a SQL Server database, starts worker threads, and exposes the dashboard at \u003ccode\u003e/hangfire\u003c/code\u003e. The dashboard provides real-time visibility into job states: succeeded, failed, processing, scheduled, and enqueued. You can manually trigger recurring jobs, delete failed jobs, or re-enqueue them for retry.\u003c/p\u003e\n\u003cp\u003eThird, enqueue jobs from anywhere in your application—controllers, services, background tasks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eControllerBase\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [HttpPost]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eIActionResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eBackgroundJob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnqueue\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIOrderProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eAccepted\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe controller responds immediately with \u003ccode\u003e202 Accepted\u003c/code\u003e. The \u003ccode\u003eProcessAsync\u003c/code\u003e method executes asynchronously on a worker thread. If processing fails—database timeout, external API unavailable—Hangfire automatically retries it up to ten times with exponential backoff (configurable). Failed jobs appear in the dashboard with full stack traces, enabling debugging without log archaeology.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"scheduling-recurring-jobs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#scheduling-recurring-jobs\" title=\"Scheduling Recurring Jobs\"\u003eScheduling Recurring Jobs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eRecurring jobs use cron expressions:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRecurringJob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddOrUpdate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;nightly-report\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGenerateReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCron\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDaily\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 2 AM daily\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eHangfire stores the recurring job definition in the database and triggers it based on the cron schedule. If the application is down during the scheduled time, Hangfire executes the job as soon as a server starts. This \u0026ldquo;catch-up\u0026rdquo; behavior prevents missed executions but can cause bursts if the application was offline for extended periods.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"retry-policies-and-error-handling\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#retry-policies-and-error-handling\" title=\"Retry Policies and Error Handling\"\u003eRetry Policies and Error Handling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTransient failures—network timeouts, temporary database unavailability—shouldn\u0026rsquo;t cause permanent job failures. Hangfire\u0026rsquo;s automatic retry mechanism handles these transparently.\u003c/p\u003e\n\u003cp\u003eBy default, failed jobs retry up to ten times with exponential backoff: immediate retry, then 1 minute, 2 minutes, 4 minutes, and so on. If all retries exhaust, the job transitions to the \u003ccode\u003eFailed\u003c/code\u003e state and appears in the dashboard. Administrators can manually re-enqueue failed jobs or investigate root causes using stack traces recorded in the database.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"writing-custom-retry-filters\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#writing-custom-retry-filters\" title=\"Writing Custom Retry Filters\"\u003eWriting Custom Retry Filters\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCustom retry logic uses filters:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomRetryAttribute\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eJobFilterAttribute\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIElectStateFilter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eOnStateElection\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eElectStateContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003efailedState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCandidateState\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"n\"\u003eFailedState\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efailedState\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCandidateState\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eScheduledState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeSpan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFromMinutes\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[CustomRetry]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eUnreliableTask\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Custom retry: wait 5 minutes, then retry indefinitely\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis filter intercepts state transitions and reschedules failed jobs with custom delays. Use cases include rate-limited APIs (retry after a cooldown), scheduled maintenance windows (skip retries during known outages), or critical workflows requiring infinite retries until manual intervention.\u003c/p\u003e\n\u003cp\u003eHangfire also supports idempotency checks via filters. If a job should only execute once regardless of retries—for example, charging a customer\u0026rsquo;s credit card—wrap the logic in idempotency tokens or database locks to prevent duplicate execution.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"scalability-from-single-instance-to-distributed-workers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#scalability-from-single-instance-to-distributed-workers\" title=\"Scalability: From Single Instance to Distributed Workers\"\u003eScalability: From Single Instance to Distributed Workers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire scales vertically and horizontally. Vertical scaling increases worker threads on a single server. Horizontal scaling adds more servers, each running its own Hangfire server instance. Workers across all servers poll the same database, coordinating via atomic database operations to prevent duplicate job processing.\u003c/p\u003e\n\u003cp\u003eWhen you deploy three application instances, each with five worker threads, you effectively have fifteen workers competing for jobs. Hangfire\u0026rsquo;s SQL-based storage uses \u003ccode\u003eUPDLOCK\u003c/code\u003e and \u003ccode\u003eREADPAST\u003c/code\u003e to ensure only one worker claims each job. This coordination happens at the database level—no external message broker or distributed lock manager required.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-switch-from-sql-to-redis\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#when-to-switch-from-sql-to-redis\" title=\"When To Switch From SQL To Redis\"\u003eWhen To Switch From SQL To Redis\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor high-throughput scenarios—tens of thousands of jobs per minute—SQL Server\u0026rsquo;s polling overhead becomes noticeable. Each worker queries the database every few seconds, creating connection churn and CPU load. Redis-based storage reduces this overhead by leveraging Redis\u0026rsquo;s pub/sub for instant job notifications instead of polling. Workers sleep until Redis signals a new job, eliminating unnecessary queries.\u003c/p\u003e\n\u003cp\u003eSwitching to Redis:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet add package Hangfire.Pro.Redis\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfire\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseRedisStorage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;localhost:6379\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRedis also supports job prioritization, faster dashboard queries, and lower database load. The trade-off: Redis is eventually consistent, so job visibility (dashboard updates) may lag slightly compared to SQL Server\u0026rsquo;s strong consistency.\u003c/p\u003e\n\u003cp\u003eAnother scalability concern: long-running jobs. If a job takes an hour to complete, it ties up a worker thread for that duration. Consider splitting long-running jobs into smaller units or processing them on dedicated servers with higher worker counts. Hangfire\u0026rsquo;s queue-based architecture supports this: route long-running jobs to a specific queue processed by dedicated servers.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eHangfire.States\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eBackgroundJob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnqueue\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportGenerator\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGenerateLargeReport\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEnqueuedState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;reports\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eConfigure a dedicated server to process only the \u003ccode\u003ereports\u003c/code\u003e queue:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddHangfireServer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eQueues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;reports\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWorkerCount\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Limit to two concurrent reports\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis isolates resource-intensive jobs from standard background work, preventing them from starving other tasks.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"dashboard-and-observability\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#dashboard-and-observability\" title=\"Dashboard and Observability\"\u003eDashboard and Observability\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire\u0026rsquo;s dashboard is one of its most compelling features. It provides real-time visibility into job states without requiring custom telemetry or logging integration.\u003c/p\u003e\n\u003cp\u003eThe dashboard displays:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEnqueued jobs\u003c/strong\u003e: Waiting for worker threads.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProcessing jobs\u003c/strong\u003e: Currently executing, with elapsed time and server information.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScheduled jobs\u003c/strong\u003e: Delayed or recurring jobs awaiting their trigger time.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSucceeded jobs\u003c/strong\u003e: Completed successfully, with execution duration.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFailed jobs\u003c/strong\u003e: Errors, stack traces, and retry counts.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRecurring jobs\u003c/strong\u003e: Cron schedules, last execution time, next execution time.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAdministrators can manually trigger recurring jobs, delete failed jobs, or re-enqueue them for retry—all from the dashboard without writing code or deploying updates. This operational flexibility reduces time spent diagnosing background job issues.\u003c/p\u003e\n\u003cp\u003eSecurity considerations: the dashboard exposes sensitive information—job parameters, stack traces, server names. Protect it using authentication middleware:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseHangfireDashboard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/hangfire\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDashboardOptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAuthorization\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eMyAuthorizationFilter\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eImplement \u003ccode\u003eIDashboardAuthorizationFilter\u003c/code\u003e to restrict access based on roles, authentication status, or IP address.\u003c/p\u003e\n\u003cp\u003eFor production systems, consider integrating Hangfire with external monitoring tools. Export job metrics—succeeded jobs per minute, average execution time, retry rates—to Prometheus, Application Insights, or Datadog. Hangfire\u0026rsquo;s extensibility via filters and listeners makes this straightforward.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-hangfire-fits\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#when-hangfire-fits\" title=\"When Hangfire Fits\"\u003eWhen Hangfire Fits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire excels in scenarios where:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence is non-negotiable\u003c/strong\u003e: Jobs must survive application restarts, deployments, or server reboots. Examples: user-initiated reports, data imports, long-running workflows.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eObservability matters\u003c/strong\u003e: Teams need real-time visibility into job states without building custom dashboards or integrating logging frameworks.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eWeb applications dominate your architecture\u003c/strong\u003e: Hangfire integrates seamlessly with ASP.NET Core, leveraging existing database infrastructure without requiring separate message brokers or coordination services.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eModerate throughput suffices\u003c/strong\u003e: Thousands of jobs per minute work well. If you need hundreds of thousands, consider Redis-based storage or evaluate Quartz.NET for advanced clustering.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAutomatic retries reduce operational burden\u003c/strong\u003e: Teams that value hands-off error handling benefit from Hangfire\u0026rsquo;s built-in retry policies, eliminating custom retry logic.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHangfire is less suitable when:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStateless deployments are required\u003c/strong\u003e: Kubernetes environments favoring ephemeral pods may prefer in-memory schedulers like NCronJob, though Hangfire\u0026rsquo;s database dependency isn\u0026rsquo;t prohibitive if managed databases are available.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSub-second latency is critical\u003c/strong\u003e: Hangfire\u0026rsquo;s polling mechanism introduces latency (typically 1-5 seconds). Real-time event-driven systems might prefer message brokers like RabbitMQ or Azure Service Bus.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eComplex scheduling is paramount\u003c/strong\u003e: While Hangfire supports cron expressions, it lacks Quartz.NET\u0026rsquo;s advanced features like job calendars, misfire handling, or priority-based execution.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"operational-benefits-and-trade-offs\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#operational-benefits-and-trade-offs\" title=\"Operational Benefits and Trade-offs\"\u003eOperational Benefits and Trade-offs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire\u0026rsquo;s primary operational benefit is reliability. Jobs stored in a database won\u0026rsquo;t vanish due to application crashes or restarts. Administrators gain confidence that critical workflows—nightly data synchronization, scheduled email campaigns, periodic cache refreshes—execute reliably even during infrastructure turbulence.\u003c/p\u003e\n\u003cp\u003eThe dashboard reduces debugging time. Instead of parsing logs to determine whether a job ran, succeeded, or failed, teams view job states in real-time. Failed jobs display stack traces inline, enabling root cause analysis without log aggregation tools.\u003c/p\u003e\n\u003cp\u003eAutomatic retries reduce operational overhead. Transient failures—network blips, temporary service unavailability—self-heal without manual intervention. Teams spend less time monitoring background jobs and more time building features.\u003c/p\u003e\n\u003cp\u003eThe trade-offs: database dependency and polling overhead. Teams must provision and maintain a database, configure connection strings, and monitor database health. In cloud environments, this might mean managed SQL instances (Azure SQL, Amazon RDS) with associated costs. Polling introduces latency and database load—acceptable for most workloads but noticeable in high-throughput or latency-sensitive scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHangfire occupies the middle ground between simplicity and enterprise-grade features. It provides persistence without requiring clustering, visibility without custom telemetry, and retries without manual logic. For ASP.NET Core applications needing reliable background processing, Hangfire delivers substantial value with moderate operational complexity.\u003c/p\u003e\n\u003cp\u003eConsider Hangfire if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYour application uses SQL Server, PostgreSQL, or Redis.\u003c/li\u003e\n\u003cli\u003eJobs must survive restarts and benefit from automatic retries.\u003c/li\u003e\n\u003cli\u003eYou value built-in dashboards over custom monitoring solutions.\u003c/li\u003e\n\u003cli\u003eThroughput requirements are moderate (thousands per minute, not hundreds of thousands).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAvoid Hangfire if:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYou need stateless, zero-dependency deployments (see NCronJob or Coravel).\u003c/li\u003e\n\u003cli\u003eComplex scheduling with calendars and advanced triggers is essential (see Quartz.NET).\u003c/li\u003e\n\u003cli\u003eUltra-low latency or extremely high throughput is required (consider message brokers or TickerQ).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe next article explores Quartz.NET, a framework that extends Hangfire\u0026rsquo;s persistence model with enterprise-grade features: clustering, advanced scheduling semantics, and multi-datacenter coordination. Where Hangfire simplifies reliability for web applications, Quartz.NET targets systems with complex scheduling demands and high-scale distributed deployments.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-27T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-2-hangfire/","language":"en","summary":"How Hangfire delivers persistent background processing with built-in dashboards, automatic retries, and distributed job execution for web applications.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — Hangfire and Persistent Reliability","url":"https://daily-devops.net/posts/dotnet-job-scheduling-2-hangfire/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA backend service receives a customer order at 14:37. The order needs fulfillment, but inventory must be validated, payment authorized, and a confirmation email dispatched. Processing these steps synchronously would lock the HTTP request thread for seconds—unacceptable when hundreds of concurrent users expect instant responses. The solution: offload the work to a background scheduler that handles tasks asynchronously, outside the request pipeline, with guaranteed execution and resilience against failures.\u003c/p\u003e\n\u003cp\u003eThis is the domain of job scheduling, and in .NET, the ecosystem offers a spectrum of solutions—from simple in-memory task runners suitable for internal tools, to enterprise-grade orchestration engines that coordinate work across distributed clusters. Choosing the wrong approach can lead to brittle systems where background jobs fail silently, retry logic becomes unmanageable, or scaling requirements force costly rewrites.\u003c/p\u003e\n\u003cp\u003eThis series examines several frameworks that span this spectrum, each occupying a distinct position defined by its architectural trade-offs—persistence versus simplicity, clustering versus overhead, compile-time safety versus runtime flexibility. Understanding where each framework excels and where it imposes constraints allows you to select the scheduler that matches your system\u0026rsquo;s operational profile, not the one with the most GitHub stars.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-background-processing-matters\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#why-background-processing-matters\" title=\"Why Background Processing Matters\"\u003eWhy Background Processing Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModern cloud-native applications demand asynchronous execution. HTTP requests must complete quickly; operations like file processing, report generation, or third-party API calls cannot block user interactions. Background jobs decouple time-intensive work from request handling, improving responsiveness and system throughput.\u003c/p\u003e\n\u003cp\u003eConsider a SaaS platform that generates monthly invoices. Generating a single PDF might take 500ms; for 10,000 customers, that\u0026rsquo;s over 80 minutes if processed serially. A background scheduler distributes this workload across multiple workers, processes jobs in parallel, and ensures that transient failures—network timeouts, temporary database unavailability—trigger automatic retries rather than silent data loss.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-manual-timer-loops-fail\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#why-manual-timer-loops-fail\" title=\"Why Manual Timer Loops Fail\"\u003eWhy Manual Timer Loops Fail\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWithout a scheduler, developers resort to manual implementations using \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e or \u003ccode\u003eTask.Delay\u003c/code\u003e wrapped in endless loops. These approaches lack persistence: if the application restarts, queued work disappears. They lack observability: tracking which jobs ran, which failed, and why becomes guesswork. They lack coordination: running multiple instances simultaneously can cause duplicate execution or race conditions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-a-scheduler-provides\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#what-a-scheduler-provides\" title=\"What A Scheduler Provides\"\u003eWhat A Scheduler Provides\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA job scheduler abstracts these concerns. It provides:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e: Jobs survive application restarts because they\u0026rsquo;re stored in a database or message queue.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRetry logic\u003c/strong\u003e: Failed jobs automatically re-execute based on configurable policies.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScheduling semantics\u003c/strong\u003e: Cron expressions, delayed execution, recurring intervals—without manual date arithmetic.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonitoring\u003c/strong\u003e: Built-in visibility into job states, execution history, and failure patterns.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScalability\u003c/strong\u003e: Distributing work across multiple server instances with load balancing and failover.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe value is operational. Teams that rely on schedulers reduce debugging time spent chasing \u0026ldquo;lost\u0026rdquo; background tasks, avoid building custom retry mechanisms, and gain confidence that critical workflows—nightly data imports, periodic cache refreshes, scheduled email campaigns—execute reliably even when infrastructure hiccups.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-evolution-from-timers-to-schedulers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#the-evolution-from-timers-to-schedulers\" title=\"The Evolution from Timers to Schedulers\"\u003eThe Evolution from Timers to Schedulers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEarly .NET applications used \u003ccode\u003eSystem.Timers.Timer\u003c/code\u003e or Windows Task Scheduler to trigger background work. These tools were adequate for simple scenarios: run a cleanup job every night at 2 AM. But as systems grew more complex, limitations surfaced.\u003c/p\u003e\n\u003cp\u003eTimers live in memory. If the process crashes, the timer state is lost. There\u0026rsquo;s no record of what ran, when it started, or why it failed. Debugging requires log archaeology. Scaling horizontally—running multiple application instances—introduces coordination challenges: multiple timers firing simultaneously can duplicate work or create contention over shared resources.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"limits-of-windows-task-scheduler\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#limits-of-windows-task-scheduler\" title=\"Limits Of Windows Task Scheduler\"\u003eLimits Of Windows Task Scheduler\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWindows Task Scheduler operates outside the application, requiring XML configuration files and administrative access to schedule tasks. Integration with application logic is indirect—typically invoking console executables that bootstrap the full application context just to run a single method. Dependency injection, logging frameworks, and application configuration require manual wiring. Updates to scheduled tasks involve modifying server configurations, not deploying code.\u003c/p\u003e\n\u003cp\u003eThese pain points drove the adoption of in-process schedulers that integrate directly with application frameworks like ASP.NET Core. Frameworks like \u003cstrong\u003eIHostedService\u003c/strong\u003e provided a native hook for long-running background operations, but developers still had to implement scheduling logic, persistence, and retry strategies manually.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"from-infrastructure-to-declaring-intent\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#from-infrastructure-to-declaring-intent\" title=\"From Infrastructure To Declaring Intent\"\u003eFrom Infrastructure To Declaring Intent\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern job schedulers abstract this complexity. They provide structured APIs for defining jobs, flexible storage backends for persistence, and runtime engines that handle execution, retries, and coordination automatically. The shift is from managing infrastructure to declaring intent: \u0026ldquo;run this job every Monday at 9 AM\u0026rdquo; becomes a single line of configuration, and the scheduler handles the rest.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"defining-the-spectrum-simplicity-to-scale\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#defining-the-spectrum-simplicity-to-scale\" title=\"Defining the Spectrum: Simplicity to Scale\"\u003eDefining the Spectrum: Simplicity to Scale\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJob scheduling frameworks occupy distinct positions on a spectrum defined by two competing priorities: \u003cstrong\u003esimplicity\u003c/strong\u003e and \u003cstrong\u003econtrol\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eOn one end, frameworks prioritize ease of integration. They minimize configuration, require no external dependencies like databases or message queues, and work out-of-the-box for small to medium applications. These are ideal for microservices, internal tools, or systems where background processing is a secondary concern. The trade-off: limited scalability, no clustering support, and jobs confined to a single process.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-enterprise-end-of-the-spectrum\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#the-enterprise-end-of-the-spectrum\" title=\"The Enterprise End Of The Spectrum\"\u003eThe Enterprise End Of The Spectrum\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eOn the other end, frameworks offer enterprise-grade features: persistent job storage with database backends, distributed coordination across server clusters, advanced scheduling with calendars and priority queues, and rich monitoring dashboards. These handle demanding workloads—thousands of jobs per minute, multi-tenant isolation, geographically distributed workers. The trade-off: increased operational complexity, external infrastructure requirements, and steeper learning curves.\u003c/p\u003e\n\u003cp\u003eSelecting a framework requires matching your system\u0026rsquo;s operational profile to these fundamental trade-offs. Do you need jobs that survive application restarts? Does your workload demand horizontal scaling across multiple instances? Are advanced scheduling semantics—business calendars, priority queues, misfire policies—essential, or would simple cron expressions suffice? Understanding these requirements shapes which end of the spectrum fits your architecture.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architectural-considerations\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#architectural-considerations\" title=\"Architectural Considerations\"\u003eArchitectural Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBeyond individual framework capabilities, several architectural factors influence scheduler selection:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence requirements\u003c/strong\u003e: If jobs must survive application restarts—for example, user-initiated reports that take minutes to generate—you need database-backed persistence. Frameworks like Hangfire, Quartz.NET, and TickerQ support this. If jobs are transient—cache warming, health checks—in-memory schedulers like NCronJob or Coravel suffice.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScalability and distribution\u003c/strong\u003e: Running a single application instance simplifies deployment but limits throughput. Multiple instances require coordination to prevent duplicate job execution. Quartz.NET\u0026rsquo;s clustering uses database locks to ensure only one instance processes each job. Hangfire distributes jobs across workers using queue-based polling. NCronJob and Coravel lack built-in clustering; scaling them requires external coordination mechanisms or accepting potential duplication.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRetry and error handling\u003c/strong\u003e: Transient failures—network timeouts, temporary database unavailability—should trigger retries, not job failures. Hangfire and TickerQ provide configurable retry policies with exponential backoff. Quartz.NET supports retry through job listeners and exception handling. Coravel and NCronJob leave retry logic to the job implementation, offering flexibility but requiring more manual code.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMonitoring and observability\u003c/strong\u003e: Production systems need visibility into job execution. Hangfire\u0026rsquo;s dashboard shows queued, processing, succeeded, and failed jobs in real-time. TickerQ provides a SignalR-powered UI with live updates. Quartz.NET supports custom listeners for telemetry integration. Coravel and NCronJob rely on application logging and external monitoring tools.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIntegration with existing infrastructure\u003c/strong\u003e: If your application already uses SQL Server, Hangfire integrates seamlessly. If you rely on Redis for caching, both Hangfire and Quartz.NET offer Redis storage backends. If you prefer avoiding external dependencies, NCronJob and Coravel fit stateless or containerized deployments better.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDevelopment ergonomics\u003c/strong\u003e: Some frameworks prioritize fluent APIs and minimal boilerplate (Coravel, NCronJob). Others favor explicit configuration and type safety (TickerQ\u0026rsquo;s source generation, Quartz.NET\u0026rsquo;s builder patterns). Developer experience matters—especially in teams where background processing is one of many concerns, not the primary focus.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-decision-factors\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#key-decision-factors\" title=\"Key Decision Factors\"\u003eKey Decision Factors\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhen evaluating job scheduling frameworks, several dimensions drive selection:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePersistence\u003c/strong\u003e: In-memory schedulers suit transient workloads—cache warming, health checks—where losing queued jobs during restarts is acceptable. Database-backed schedulers ensure job durability, critical for user-initiated operations like report generation or order fulfillment.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eClustering\u003c/strong\u003e: Single-instance deployments simplify operations but limit throughput and create single points of failure. Distributed coordination enables horizontal scaling but requires infrastructure for coordination—typically database locks or distributed consensus protocols.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eScheduling complexity\u003c/strong\u003e: Simple use cases—\u0026ldquo;run daily at 2 AM\u0026rdquo;—need only cron expressions. Advanced scenarios—\u0026ldquo;last business day of the quarter, excluding holidays\u0026rdquo;—require calendar support, custom triggers, or misfire handling.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eObservability\u003c/strong\u003e: Production systems need visibility into job states. Built-in dashboards provide real-time monitoring without custom instrumentation. Frameworks without dashboards rely on application logging and external observability tools.\u003c/p\u003e\n\u003cp\u003eUnderstanding where your requirements fall on each dimension guides framework selection more effectively than popularity metrics or feature counts.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"moving-forward\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#moving-forward\" title=\"Moving Forward\"\u003eMoving Forward\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe next articles traverse the spectrum—from simple in-process scheduling to durable, distributed engines—using real scenarios to surface trade-offs in persistence, scalability, and observability. The journey starts with a pragmatic, database-backed option for web apps, then contrasts lighter in-memory approaches and heavier clustered solutions, concluding with a concise comparative guide to map requirements to the right fit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaways\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/#practical-takeaways\" title=\"Practical Takeaways\"\u003ePractical Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eJob scheduling is infrastructure that fades into the background when chosen correctly and becomes a source of friction when mismatched. Before selecting a framework, evaluate:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003ePersistence needs\u003c/strong\u003e: Do jobs need to survive restarts, or are they ephemeral?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScale requirements\u003c/strong\u003e: Single instance or distributed cluster?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOperational complexity tolerance\u003c/strong\u003e: How much infrastructure are you willing to manage?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntegration constraints\u003c/strong\u003e: What databases, message queues, or frameworks already exist in your stack?\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTeam priorities\u003c/strong\u003e: Simplicity and speed versus control and features?\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe next article begins with Hangfire, a framework that balances usability and reliability for web applications. It demonstrates how persistent job storage, automatic retries, and built-in monitoring simplify background processing without requiring clustering or external coordination.\u003c/p\u003e\n\u003cp\u003eChoosing a scheduler is choosing an operational philosophy. Pick wisely, and background jobs become invisible enablers of system capability. Pick poorly, and they become sources of operational overhead and silent failures.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-25T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling-1-landscape/","language":"en","summary":"Why background processing matters for cloud-native .NET, and how schedulers evolved from manual timers to robust, distributed orchestration engines.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — The Landscape","url":"https://daily-devops.net/posts/dotnet-job-scheduling-1-landscape/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eBackground processing is one of those things that feels trivial until it isn\u0026rsquo;t. A timer here, a \u003ccode\u003eTask.Run\u003c/code\u003e there — then you\u0026rsquo;re debugging why invoices didn\u0026rsquo;t go out on the first of the month, why the retry logic fired seventeen times, or why two app instances processed the same order simultaneously. At that point, you needed a real scheduler yesterday.\u003c/p\u003e\n\u003cp\u003eThis series exists because \u0026ldquo;.NET job scheduling\u0026rdquo; is not a single problem. It\u0026rsquo;s a spectrum of trade-offs between simplicity and control, between zero dependencies and full persistence, between in-memory execution and distributed coordination across clusters. Picking wrong means either over-engineering a microservice with a Quartz.NET cluster or hitting walls the moment a SaaS platform needs durable job storage.\u003c/p\u003e\n\u003cp\u003eSeven articles. Five frameworks. One comparative review that maps requirements to the right fit.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-background-processing-gets-complicated\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#why-background-processing-gets-complicated\" title=\"Why Background Processing Gets Complicated\"\u003eWhy Background Processing Gets Complicated\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe problems that push teams toward a real scheduler are almost never visible during development. Locally, \u003ccode\u003eTask.Run\u003c/code\u003e works fine. The job runs, the test passes, the feature ships. The production incidents show up six months later, often at the worst possible time.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-lost-job-problem\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#the-lost-job-problem\" title=\"The Lost Job Problem\"\u003eThe Lost Job Problem\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe most common failure mode is the lost job. An application restarts — a deployment, a crash, a container being evicted from a node — and any in-flight or queued work disappears with the process. In-memory scheduling has no persistence by definition. You queued fifty email notifications, the pod restarted during the send loop, and now you don\u0026rsquo;t know which ones went out and which ones didn\u0026rsquo;t. There\u0026rsquo;s no queue to inspect, no log of what ran, no way to replay. The work is simply gone.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"duplicate-execution-across-instances\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#duplicate-execution-across-instances\" title=\"Duplicate Execution Across Instances\"\u003eDuplicate Execution Across Instances\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe second failure mode is the duplicate job. Once you scale beyond a single instance — which happens quickly on any cloud-hosted service — every instance running a \u003ccode\u003eHostedService\u003c/code\u003e-based timer will fire independently. If your job sends a payment confirmation, two instances mean two emails. If it charges a credit card, two instances mean two charges. Preventing this requires distributed locking: some mechanism that ensures only one instance picks up and executes a given job at a time. Rolling that yourself is possible, but the edge cases accumulate fast. What happens when the lock holder crashes mid-execution? When the lock TTL expires before the job completes? When two instances acquire the lock within the same millisecond? Frameworks that solve this problem have already worked through those edge cases. Home-grown implementations usually haven\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"silent-errors-and-missing-retries\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#silent-errors-and-missing-retries\" title=\"Silent Errors And Missing Retries\"\u003eSilent Errors And Missing Retries\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe third failure mode is silent errors. A job throws an exception. The \u003ccode\u003eTask.Run\u003c/code\u003e wrapper swallows it, or logs it once, and moves on. Nobody knows the job failed. Nobody retries it. The downstream system it was supposed to update is now inconsistent, and the inconsistency accumulates until something upstream notices. Real schedulers give you retry policies — exponential backoff, maximum attempt counts, dead-letter queues for jobs that exhaust their retries. They give you visibility into what failed, when it failed, and why. That visibility doesn\u0026rsquo;t exist when your scheduling layer is a \u003ccode\u003eSystem.Threading.Timer\u003c/code\u003e and a try-catch block.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"operational-blindness-at-runtime\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#operational-blindness-at-runtime\" title=\"Operational Blindness At Runtime\"\u003eOperational Blindness At Runtime\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe fourth failure mode is operational blindness. Even when jobs succeed, in-memory scheduling gives you nothing to observe at runtime. You can\u0026rsquo;t see what\u0026rsquo;s queued, what\u0026rsquo;s running, what ran an hour ago. You can\u0026rsquo;t pause a job that\u0026rsquo;s misbehaving without deploying a code change. You can\u0026rsquo;t trigger a one-off execution without building an admin endpoint. The moment background processing becomes important to the business — not just a convenience — this blindness becomes a liability.\u003c/p\u003e\n\u003cp\u003eNone of these problems are hypothetical. They show up on teams that made perfectly reasonable decisions early in a project and then found those decisions didn\u0026rsquo;t scale to their operational requirements. The goal of this series is to make the trade-offs explicit before you hit them in production.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-series-covers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#what-this-series-covers\" title=\"What This Series Covers\"\u003eWhat This Series Covers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePart 1 — \u003ca href=\"/posts/dotnet-job-scheduling-1-landscape/\"\u003eThe Landscape\u003c/a\u003e\u003c/strong\u003e sets the foundation. Why background processing matters, how the ecosystem evolved from raw timers to modern schedulers, and what architectural dimensions actually drive framework selection: persistence, clustering, observability, retry behavior, and development ergonomics.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 2 — \u003ca href=\"/posts/dotnet-job-scheduling-2-hangfire/\"\u003eHangfire and Persistent Reliability\u003c/a\u003e\u003c/strong\u003e covers the framework that balances usability and reliability for web applications. Persistent job storage in SQL Server or Redis, automatic retries, a built-in monitoring dashboard, distributed execution across multiple workers — all without requiring clustering infrastructure. The practical choice for ASP.NET Core applications that need durability without complexity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 3 — \u003ca href=\"/posts/dotnet-job-scheduling-3-quartznet/\"\u003eQuartz.NET for Enterprise Scale\u003c/a\u003e\u003c/strong\u003e examines the framework that ports Java\u0026rsquo;s Quartz directly to .NET. Enterprise-grade clustering with database-coordinated distributed locking, advanced triggers, job calendars for business-day scheduling, and multi-datacenter coordination. The right tool when workloads push into thousands of jobs per minute or require sophisticated scheduling semantics — and the wrong tool for most other situations.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 4 — \u003ca href=\"/posts/dotnet-job-scheduling-4-coravel/\"\u003eCoravel and Fluent Simplicity\u003c/a\u003e\u003c/strong\u003e shows the opposite end of the spectrum. No database, no external dependencies, no infrastructure overhead. Coravel integrates directly with \u003ccode\u003eIServiceCollection\u003c/code\u003e, schedules jobs through a readable fluent API, and gets out of the way. The answer for internal tools, small services, or any application where background processing is a secondary concern rather than a core requirement.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 5 — \u003ca href=\"/posts/dotnet-job-scheduling-5-ncronjob/\"\u003eNCronJob and Native Minimalism\u003c/a\u003e\u003c/strong\u003e covers the ASP.NET Core–native scheduler built around \u003ccode\u003eIHostedService\u003c/code\u003e. Zero dependencies, cron expressions, execution contexts with cancellation support — and nothing else. NCronJob targets containerized microservices where stateless scheduling is sufficient and adding database dependencies would create more problems than it solves.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 6 — \u003ca href=\"/posts/dotnet-job-scheduling-6-tickerq/\"\u003eTickerQ and Modern Architecture\u003c/a\u003e\u003c/strong\u003e examines the youngest framework in the series. Source generation eliminates reflection-based job registration. EF Core handles persistence. A SignalR-powered real-time dashboard replaces polling-based UIs. TickerQ makes different bets than Hangfire — compile-time safety over convention, async-first execution, and a smaller surface area.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePart 7 — \u003ca href=\"/posts/dotnet-job-scheduling-7-comparative-review/\"\u003eChoosing the Right Framework\u003c/a\u003e\u003c/strong\u003e synthesizes the series into decision guidance. Feature matrices across persistence, clustering, dashboards, retry policies, cron support, and scheduling complexity. Suitability ratings across operational dimensions. Decision heuristics grounded in system maturity and infrastructure constraints rather than GitHub star counts.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"who-this-is-for\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#who-this-is-for\" title=\"Who This Is For\"\u003eWho This Is For\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;re a .NET developer or architect evaluating background processing options — either for a new project or because the current approach is causing operational pain. You want to understand trade-offs rather than just copy configuration snippets.\u003c/p\u003e\n\u003cp\u003eThe series assumes familiarity with ASP.NET Core and dependency injection. Code examples use \u003ccode\u003eIHostedService\u003c/code\u003e, \u003ccode\u003eIServiceCollection\u003c/code\u003e, and Entity Framework where relevant. Infrastructure examples reference SQL Server, Redis, and Azure — but the architectural conclusions apply regardless of cloud provider.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re already running Hangfire or Quartz.NET in production and wondering whether you made the right call, the comparative review in Part 7 is the right starting point. If you\u0026rsquo;re starting fresh and trying to understand the landscape before committing to a framework, Part 1 gives you the context to make that decision with open eyes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-short-answer\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#the-short-answer\" title=\"The Short Answer\"\u003eThe Short Answer\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you need one sentence: use \u003cstrong\u003eHangfire\u003c/strong\u003e unless you have a specific reason not to. It handles the 80% case — durable background jobs in web applications — with minimal setup and a built-in dashboard that makes production operation visible.\u003c/p\u003e\n\u003cp\u003eReach for \u003cstrong\u003eQuartz.NET\u003c/strong\u003e when you need clustering across multiple application instances or advanced scheduling semantics like business calendars. Accept the operational complexity as a deliberate trade-off, not a necessary cost.\u003c/p\u003e\n\u003cp\u003eChoose \u003cstrong\u003eCoravel\u003c/strong\u003e or \u003cstrong\u003eNCronJob\u003c/strong\u003e when you specifically don\u0026rsquo;t want persistence — for stateless containers, internal tools, or cache warming where losing queued work on restart is acceptable.\u003c/p\u003e\n\u003cp\u003eConsider \u003cstrong\u003eTickerQ\u003c/strong\u003e if source generation and compile-time safety matter more than ecosystem maturity, or if you want EF Core integration without building it yourself.\u003c/p\u003e\n\u003cp\u003eThe comparative review in Part 7 maps these heuristics to concrete scenarios with more nuance.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-series-is-not\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#what-this-series-is-not\" title=\"What This Series Is Not\"\u003eWhat This Series Is Not\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIt\u0026rsquo;s worth being explicit about what this series doesn\u0026rsquo;t cover, because the .NET background processing space is broader than in-process schedulers.\u003c/p\u003e\n\u003cp\u003eThis series does not cover \u003cstrong\u003eAzure Functions\u003c/strong\u003e or any other serverless compute model. Functions-based scheduling — cron triggers, timer triggers, queue-triggered functions — solves a related but distinct problem. The infrastructure model is fundamentally different: you\u0026rsquo;re not running a persistent process, you\u0026rsquo;re invoking isolated functions on demand. If your workload fits serverless, that\u0026rsquo;s a legitimate and often cheaper choice. It just isn\u0026rsquo;t the same trade-off space as embedding a scheduler inside a long-running ASP.NET Core application. The operational characteristics are different, the scaling model is different, and the failure modes are different. Treating them as interchangeable leads to bad decisions in both directions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-message-queues-are-not-schedulers\"\u003e\u003ca href=\"/posts/dotnet-job-scheduling/#why-message-queues-are-not-schedulers\" title=\"Why Message Queues Are Not Schedulers\"\u003eWhy Message Queues Are Not Schedulers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis series does not cover \u003cstrong\u003eAzure Service Bus\u003c/strong\u003e, \u003cstrong\u003eRabbitMQ\u003c/strong\u003e, or distributed message queues in general. Message queues and job schedulers overlap in some scenarios — both can defer work, both support retry semantics — but they\u0026rsquo;re architecturally different. A message queue is a communication channel between services. A job scheduler is an execution engine within a service. Using Service Bus as a job queue is valid; this series doesn\u0026rsquo;t tell you how to do it. If you\u0026rsquo;re building a system where the producer and consumer are different services, a message queue is likely the right abstraction. If you\u0026rsquo;re building a system where background jobs run inside the same process as the web application, an embedded scheduler is what you want.\u003c/p\u003e\n\u003cp\u003eThis series does not cover \u003cstrong\u003eactor-model frameworks\u003c/strong\u003e like Akka.NET or Orleans. Actor models can schedule and coordinate distributed work, but they represent a significantly different programming model and architectural commitment. The virtual actor model in Orleans gives you scheduling primitives, grain timers, and reminder services that persist across grain deactivations. That\u0026rsquo;s genuinely powerful for certain workloads — but adopting Orleans to get durable job scheduling is a large investment. If you\u0026rsquo;re already committed to an actor model, you have better options than adding a separate scheduler. If you\u0026rsquo;re not, adding a scheduler is almost certainly simpler than adopting an actor model.\u003c/p\u003e\n\u003cp\u003eThis series also does not benchmark raw throughput in any systematic way. You\u0026rsquo;ll find numbers in the individual articles where they\u0026rsquo;re meaningful, but throughput comparisons between in-memory and persistent schedulers are rarely the deciding factor in framework selection. A persistent scheduler writing jobs to SQL Server will always be slower than an in-memory scheduler. That\u0026rsquo;s expected. The question is whether the throughput floor of the persistent option is acceptable for your workload — and for the vast majority of applications that actually need persistence, the answer is yes. Chasing throughput numbers while ignoring operational requirements is how teams end up with fast schedulers they can\u0026rsquo;t operate.\u003c/p\u003e\n\u003cp\u003eWhat this series does focus on is the practical decision of which framework to embed in an ASP.NET Core application when you need background jobs that survive restarts, don\u0026rsquo;t duplicate across instances, fail visibly, and can be operated by someone who wasn\u0026rsquo;t the original developer. That scope is narrow enough to be useful.\u003c/p\u003e\n","date_modified":"2026-05-25T23:41:10+02:00","date_published":"2025-11-25T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-job-scheduling/","language":"en","summary":"Seven articles comparing Hangfire, Quartz.NET, Coravel, NCronJob, and TickerQ—match each .NET job scheduler to the workloads it actually fits.","tags":["dotnet","csharp","architecture","nuget","softwareengineering"],"title":".NET Job Scheduling — The Complete Series","url":"https://daily-devops.net/posts/dotnet-job-scheduling/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eMicrosoft just did something unusual: \u003cem\u003ethey fixed a problem before most people realized they had it.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eFor years, \u003ccode\u003edotnet test\u003c/code\u003e wasn\u0026rsquo;t really a test runner—it was actually just a wrapper around \u003ccode\u003evstest.console.exe\u003c/code\u003e, a legacy artifact from the pre-.NET-Core era that Microsoft couldn\u0026rsquo;t quite kill. It worked, mostly, if you didn\u0026rsquo;t think too hard about why your tests sometimes behaved differently in Visual Studio than in GitHub Actions, or why test discovery occasionally took longer than the tests themselves.\u003c/p\u003e\n\u003cp\u003eWith .NET 10, Microsoft has finally integrated testing directly into the SDK through \u003cstrong\u003eMicrosoft.Testing.Platform (MTP)\u003c/strong\u003e. The old VSTest infrastructure is now out. The new system runs tests in-process, unifies behavior across environments, and—this is actually the important part—finally respects your configuration files.\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s a catch, of course. There always is.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"from-test-wrapper-to-test-platform\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#from-test-wrapper-to-test-platform\" title=\"From Test Wrapper to Test Platform\"\u003eFrom Test Wrapper to Test Platform\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eRunning tests in .NET used to mean choosing a framework—\u003ca href=\"https://xunit.net/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003exUnit\u003c/a\u003e, \u003ca href=\"https://nunit.org/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNUnit\u003c/a\u003e, \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMSTest\u003c/a\u003e, or the newer \u003ca href=\"https://tunit.dev/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eTUnit\u003c/a\u003e—and then essentially just hoping \u003ccode\u003edotnet test\u003c/code\u003e could somehow figure out how to talk to it. Each framework had its own test adapter. Each adapter had its own quirks. Your CI pipeline basically just crossed its fingers and hoped for green checkmarks.\u003c/p\u003e\n\u003cp\u003eThe result? Test execution that varied subtly between your laptop, your colleague\u0026rsquo;s laptop, and the build server. Debugging test failures meant first figuring out \u003cem\u003ewhich version of which adapter was running where\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003eMicrosoft.Testing.Platform changes that architecture. Instead of spawning separate processes and negotiating through adapters, MTP embeds the test runner directly into the SDK. Discovery, execution, and reporting now follow a single, predictable path. Tests run in-process. The CLI is cleaner. The performance is measurably better in projects with large test suites.\u003c/p\u003e\n\u003cp\u003eEnabling it requires exactly four lines in your \u003ccode\u003eglobal.json\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;test\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;runner\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Microsoft.Testing.Platform\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNo SDK pinning required. No complicated setup. Just those four lines, and .NET 10 switches to the new test engine automatically.\u003c/p\u003e\n\u003cp\u003eThe simplicity is almost suspicious.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-actually-improves-and-what-doesnt\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#what-actually-improves-and-what-doesnt\" title=\"What Actually Improves (And What Doesn\u0026rsquo;t)\"\u003eWhat Actually Improves (And What Doesn\u0026rsquo;t)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s be specific. MTP isn\u0026rsquo;t magic—it\u0026rsquo;s engineering. Here\u0026rsquo;s what changes when you enable it:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTest discovery is faster.\u003c/strong\u003e In a project with ~3,500 tests, discovery dropped from 8 seconds to under 3 on my local machine. That\u0026rsquo;s honestly not earth-shattering, but it\u0026rsquo;s definitely noticeable when you\u0026rsquo;re running focused test sets repeatedly during development. Over a typical workday with 50 test runs? That actually saves roughly 4 minutes. Not revolutionary, but certainly not nothing either.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe CLI makes sense now.\u003c/strong\u003e Previously, \u003ccode\u003edotnet test --filter\u003c/code\u003e required arcane syntax and those bizarre \u003ccode\u003e--\u003c/code\u003e separators to pass arguments through to the adapter. MTP removes that layer of indirection. The commands do what you\u0026rsquo;d expect without translation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEnvironment consistency improves.\u003c/strong\u003e Because the test runner is part of the SDK, your local machine and your CI pipeline execute tests the same way—assuming you actually configure your pipeline correctly (more on that disaster shortly).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBut performance gains aren\u0026rsquo;t universal.\u003c/strong\u003e If your tests are already fast, you probably won\u0026rsquo;t see dramatic improvements. MTP mainly optimizes infrastructure overhead, not slow database calls or badly written assertions. Don\u0026rsquo;t expect miracles if your test suite still takes 20 minutes because it\u0026rsquo;s hitting real APIs.\u003c/p\u003e\n\u003cp\u003eAnd here\u0026rsquo;s the part Microsoft doesn\u0026rsquo;t emphasize: \u003cstrong\u003eMTP won\u0026rsquo;t save you from bad tests.\u003c/strong\u003e If your test suite is flaky, brittle, or poorly isolated, the new platform just runs that mess faster.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-about-visual-studio-integration\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#what-about-visual-studio-integration\" title=\"What about Visual Studio integration?\"\u003eWhat about Visual Studio integration?\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eVisual Studio 17.14 or later integrates with MTP. Earlier versions rely on VSTest and may behave differently. If your team uses mixed VS versions, validate results locally with the CLI to avoid IDE-specific discrepancies.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-ci-pipeline-trap-and-how-to-avoid-it\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#the-ci-pipeline-trap-and-how-to-avoid-it\" title=\"The CI Pipeline Trap (And How to Avoid It)\"\u003eThe CI Pipeline Trap (And How to Avoid It)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s where things get entertaining.\u003c/p\u003e\n\u003cp\u003eYou add that \u003ccode\u003eglobal.json\u003c/code\u003e snippet. Tests run perfectly on your machine. You commit, push, and watch your GitHub Actions pipeline\u0026hellip; fail spectacularly.\u003c/p\u003e\n\u003cp\u003eWhy? Because GitHub\u0026rsquo;s hosted runners don\u0026rsquo;t automatically respect your \u003ccode\u003eglobal.json\u003c/code\u003e. They just use whatever SDK version happens to be installed—often an older one that doesn\u0026rsquo;t even support MTP. Your carefully configured local environment and your CI pipeline are now essentially running completely different test infrastructure.\u003c/p\u003e\n\u003cp\u003eI learned this the hard way when a colleague spent two hours debugging \u0026ldquo;flaky\u0026rdquo; tests that weren\u0026rsquo;t actually flaky at all. The tests validated timeout behavior in an async workflow—they passed consistently with MTP locally and then failed consistently with VSTest in CI. Same code, same timeout values, completely different test runner behavior. VSTest\u0026rsquo;s process isolation apparently meant slightly different timing characteristics. We only figured it out after painstakingly comparing the test execution logs line by line and finally noticing the runner version mismatch.\u003c/p\u003e\n\u003cp\u003eThe fix is one line—but you have to know it exists:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/setup-dotnet@v5\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eglobal-json-file\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;./global.json\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edotnet test\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat \u003ccode\u003eglobal-json-file\u003c/code\u003e parameter forces the action to actually read your configuration. Without it, you\u0026rsquo;re deploying tests with one runner and debugging them with another.\u003c/p\u003e\n\u003cp\u003eIf you don\u0026rsquo;t specify this explicitly, your \u003ccode\u003eglobal.json\u003c/code\u003e is basically just decorative. It just sits in your repository looking official while your pipeline ignores it completely. I\u0026rsquo;ve actually seen teams add comments to their \u003ccode\u003eglobal.json\u003c/code\u003e files carefully explaining why certain settings exist, not realizing the entire file wasn\u0026rsquo;t even being used. That\u0026rsquo;s not configuration—that\u0026rsquo;s just theater.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"version-compatibility-or-who-gets-left-behind\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#version-compatibility-or-who-gets-left-behind\" title=\"Version Compatibility (Or: Who Gets Left Behind)\"\u003eVersion Compatibility (Or: Who Gets Left Behind)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMTP doesn\u0026rsquo;t support every test framework version ever released. Microsoft drew a line, and some older projects sit on the wrong side of it.\u003c/p\u003e\n\u003cp\u003eTo use Microsoft.Testing.Platform, your test frameworks need these minimum versions:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e → Version \u003cstrong\u003e3.x\u003c/strong\u003e or later\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e → Version \u003cstrong\u003e3.2.0\u003c/strong\u003e or later\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e → \u003cstrong\u003eNUnit3TestAdapter 5.0.0\u003c/strong\u003e or later\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e → Works out of the box (it was designed with MTP in mind)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVisual Studio\u003c/strong\u003e → Version \u003cstrong\u003e17.14\u003c/strong\u003e or later for full integration\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIf you\u0026rsquo;re running older versions, the SDK simply won\u0026rsquo;t negotiate. It fails hard. No fallback to VSTest, no warning, just an error message telling you to upgrade.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s actually good design. Ambiguity in test execution creates exactly the kind of \u0026ldquo;works on my machine\u0026rdquo; disasters MTP is supposed to prevent. Better to fail explicitly than to silently run different infrastructure depending on what\u0026rsquo;s installed.\u003c/p\u003e\n\u003cp\u003eBut it does mean migration isn\u0026rsquo;t optional if you\u0026rsquo;re upgrading to .NET 10. You can\u0026rsquo;t enable MTP halfway. Either your entire test suite supports it, or you don\u0026rsquo;t use it at all.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migration-strategy-or-how-not-to-break-everything\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#migration-strategy-or-how-not-to-break-everything\" title=\"Migration Strategy (Or: How Not to Break Everything)\"\u003eMigration Strategy (Or: How Not to Break Everything)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMigrating to MTP isn\u0026rsquo;t technically complicated, but it does actually require coordination. You can\u0026rsquo;t just enable it in isolation—everyone on the team needs to be running compatible tools, or the test results will simply stop being reliable.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s a migration approach that won\u0026rsquo;t cause chaos:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e1. Audit your test framework versions first.\u003c/strong\u003e\nCheck every test project. If you\u0026rsquo;re running xUnit 2.x or MSTest 2.x, you\u0026rsquo;re upgrading before you can enable MTP. No shortcuts.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2. Add the \u003ccode\u003eglobal.json\u003c/code\u003e configuration.\u003c/strong\u003e\nStart with the minimal snippet. You don\u0026rsquo;t need to pin an SDK version unless you have specific compatibility requirements elsewhere.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3. Update your CI/CD pipelines.\u003c/strong\u003e\nAdd the \u003ccode\u003eglobal-json-file\u003c/code\u003e parameter to your \u003ccode\u003esetup-dotnet\u003c/code\u003e action. Test it on a branch before merging. Verify that the pipeline is actually using MTP by checking the test output logs.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e4. Run tests locally and in CI—compare the results.\u003c/strong\u003e\nIf they differ, you\u0026rsquo;ve found a configuration issue. Fix it now, before it becomes a debugging nightmare three months from now. Pay special attention to tests that involve timing, parallelization, or resource cleanup—these are the ones most likely to behave differently between test runners.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;ve read \u003ca href=\"/posts/tests-are-lying/\"\u003e\u0026ldquo;Your Tests Are Lying — Mutation Testing in .NET\u0026rdquo;\u003c/a\u003e, you know how dangerous it is when tests pass for the wrong reasons. MTP reduces that risk—but only if your environments are actually configured consistently.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-not-to-migrate-yes-really\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#when-not-to-migrate-yes-really\" title=\"When Not to Migrate (Yes, Really)\"\u003eWhen Not to Migrate (Yes, Really)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNot every project should rush into MTP. Here are scenarios where you might want to wait:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLegacy test suites with heavy VSTest dependencies.\u003c/strong\u003e If your tests rely on specific VSTest console runners, custom adapters, or undocumented behavior, migration will break things. You\u0026rsquo;ll need to refactor or rewrite parts of your test infrastructure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eProjects still on .NET 8 LTS.\u003c/strong\u003e MTP is a .NET 10 feature. If you\u0026rsquo;re staying on an LTS version for stability, you\u0026rsquo;re essentially stuck with VSTest. That\u0026rsquo;s fine—VSTest still works. It\u0026rsquo;s just not getting any new features.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTeams without time to validate the migration.\u003c/strong\u003e Half-migrating is worse than not migrating. If you can\u0026rsquo;t dedicate time to verify that tests behave identically across environments, defer the change until you can.\u003c/p\u003e\n\u003cp\u003eMTP is definitely an improvement, but it\u0026rsquo;s not urgent. If your current test infrastructure already works reliably, you\u0026rsquo;re really not missing out by waiting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-this-actually-means-for-your-workflow\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#what-this-actually-means-for-your-workflow\" title=\"What This Actually Means for Your Workflow\"\u003eWhat This Actually Means for Your Workflow\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe shift to MTP changes how you think about test configuration. Your \u003ccode\u003eglobal.json\u003c/code\u003e file is no longer just an SDK hint—it\u0026rsquo;s a binding contract. The SDK reads it, respects it, and enforces it. If your pipeline isn\u0026rsquo;t configured to honor that contract, your tests will diverge silently between environments.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s both the strength and the risk of this change. MTP removes ambiguity, but only if you configure it correctly everywhere. Miss one environment, and you\u0026rsquo;re back to debugging phantom failures that only reproduce in CI.\u003c/p\u003e\n\u003cp\u003eThe good news? Once configured properly, tests become predictable. The bad news? Getting there requires discipline, not just documentation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"should-you-migrate-now\"\u003e\u003ca href=\"/posts/dotnet-10-testing/#should-you-migrate-now\" title=\"Should You Migrate Now?\"\u003eShould You Migrate Now?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re already on .NET 10, yes. The benefits clearly outweigh the setup cost, especially if you\u0026rsquo;ve already dealt with flaky CI pipelines or inconsistent test behavior across environments.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re on an LTS version and your tests are stable, there\u0026rsquo;s really no rush. VSTest isn\u0026rsquo;t going anywhere immediately, and MTP will still be there when you eventually upgrade.\u003c/p\u003e\n\u003cp\u003eBut if you\u0026rsquo;re planning to move to .NET 10 anyway, enable MTP early in the migration process. It\u0026rsquo;s easier to validate test behavior during a planned upgrade than to debug it six months later when the root cause has been buried under other changes.\u003c/p\u003e\n\u003cp\u003eAdd the four lines to \u003ccode\u003eglobal.json\u003c/code\u003e. Update your CI config. Upgrade your test frameworks. Run the tests. Compare the results.\u003c/p\u003e\n\u003cp\u003eIf they match—and they should—you\u0026rsquo;re done. If they don\u0026rsquo;t, you\u0026rsquo;ve found a configuration problem that would have bitten you eventually anyway. Better to find it now during a planned migration than at 2 AM when production is down and your tests are lying to you about what\u0026rsquo;s safe to deploy.\u003c/p\u003e\n\u003cp\u003eMicrosoft fixed the test runner. Whether you use it or keep debugging phantom CI failures is your choice—but when the next \u0026ldquo;works on my machine\u0026rdquo; ticket comes in, at least you\u0026rsquo;ll know exactly why.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-20T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-10-testing/","language":"en","summary":"Microsoft.Testing.Platform replaces VSTest in .NET 10. See what improves, what breaks, and why your global.json now matters in IDE and CI reliably.\n","tags":["testing","dotnet","csharp","softwareengineering","github-actions","devops"],"title":".NET 10 Testing: Microsoft Finally Fixed the Test Runner (Mostly)\n","url":"https://daily-devops.net/posts/dotnet-10-testing/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eCode metrics have become a standard feature in modern development environments, yet their implementation and interpretation often leave much to be desired. While Visual Studio and .NET provide comprehensive code metrics analysis, the way these metrics are configured, presented, and (more critically) acted upon reveals a fundamental disconnect between measurement and meaningful improvement.\u003c/p\u003e\n\u003cp\u003eWhat code metrics actually measure, how to configure them properly, and (more importantly) why blindly following thresholds without understanding context is, frankly, a recipe for misguided refactoring efforts that waste your team\u0026rsquo;s time and actively damage your codebase.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-code-metrics-what-are-we-actually-measuring\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#understanding-code-metrics-what-are-we-actually-measuring\" title=\"Understanding Code Metrics: What Are We Actually Measuring?\"\u003eUnderstanding Code Metrics: What Are We Actually Measuring?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eVisual Studio provides several key code metrics, each designed to quantify different aspects of code complexity and maintainability. Understanding what these numbers actually represent is essential before you start making decisions based on them.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"maintainability-index-mi\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#maintainability-index-mi\" title=\"Maintainability Index (MI)\"\u003eMaintainability Index (MI)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe \u003cstrong\u003eMaintainability Index\u003c/strong\u003e is a composite metric ranging from 0 to 100, where higher values supposedly indicate better maintainability. Microsoft\u0026rsquo;s thresholds suggest:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGreen (20 to 100)\u003c/strong\u003e: Good maintainability\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYellow (10 to 19)\u003c/strong\u003e: Moderate maintainability\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRed (0 to 9)\u003c/strong\u003e: Poor maintainability\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe formula considers cyclomatic complexity, lines of code, and computational complexity. However, this single number masks important nuances. A method with a high maintainability index might still be poorly designed if it violates single responsibility principles or lacks proper abstraction. Conversely, a legitimately complex algorithm might score poorly despite being optimally implemented.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"cyclomatic-complexity\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#cyclomatic-complexity\" title=\"Cyclomatic Complexity\"\u003eCyclomatic Complexity\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis metric counts the number of linearly independent paths through a method\u0026rsquo;s code. Each \u003ccode\u003eif\u003c/code\u003e, \u003ccode\u003ewhile\u003c/code\u003e, \u003ccode\u003efor\u003c/code\u003e, \u003ccode\u003ecase\u003c/code\u003e, and logical operator (\u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e, \u003ccode\u003e||\u003c/code\u003e) increases the count. The conventional wisdom suggests:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e1 to 10\u003c/strong\u003e: Simple, low risk\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e11 to 20\u003c/strong\u003e: Moderate complexity\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e21 to 50\u003c/strong\u003e: High complexity\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e50+\u003c/strong\u003e: Untestable, critical risk\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWhile cyclomatic complexity provides valuable insights into testability, it can be seriously misleading. A well-structured method with clear guard clauses might score higher than a convoluted method with fewer branches but objectively worse logical flow. The metric measures branches, not clarity.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"lines-of-code-loc\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#lines-of-code-loc\" title=\"Lines of Code (LOC)\"\u003eLines of Code (LOC)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis counts executable lines, excluding comments and blank lines. While straightforward, LOC is perhaps the most misunderstood metric. \u003cstrong\u003eA high line count doesn\u0026rsquo;t automatically indicate poor quality\u003c/strong\u003e. It might represent thorough error handling, comprehensive validation, or simply necessary business logic. Splitting a 200-line method into ten 20-line methods doesn\u0026rsquo;t magically improve anything if those ten methods are tightly coupled and need to be understood together anyway.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"depth-of-inheritance\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#depth-of-inheritance\" title=\"Depth of Inheritance\"\u003eDepth of Inheritance\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis measures how many classes are in the inheritance hierarchy above a type. Deep inheritance trees can indicate over-engineering, but shallow hierarchies aren\u0026rsquo;t automatically better. The appropriate depth depends entirely on the domain model and design patterns being employed. Blindly flattening inheritance hierarchies to satisfy a metric threshold often results in awkward composition patterns or duplicated logic.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"class-coupling\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#class-coupling\" title=\"Class Coupling\"\u003eClass Coupling\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis counts the number of unique types a class references. High coupling suggests tight dependencies and reduced modularity, but some coupling is inevitable and even desirable when working with framework types or established patterns. A web controller that references request models, response models, service interfaces, and result types will naturally have higher coupling, and that\u0026rsquo;s perfectly fine.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-critical-flaw-arbitrary-thresholds-without-context\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#the-critical-flaw-arbitrary-thresholds-without-context\" title=\"The Critical Flaw: Arbitrary Thresholds Without Context\"\u003eThe Critical Flaw: Arbitrary Thresholds Without Context\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is where the industry consistently gets it wrong. \u003cstrong\u003eApplying universal thresholds to code metrics fundamentally ignores the reality that different code serves different purposes\u003c/strong\u003e. The notion that a cyclomatic complexity of 10 is universally \u0026ldquo;good\u0026rdquo; while 11 is suddenly \u0026ldquo;problematic\u0026rdquo; is, quite honestly, nonsense. It\u0026rsquo;s the software equivalent of declaring that all methods must be exactly 15 lines long because someone once read that in a blog post.\u003c/p\u003e\n\u003cp\u003eConsider a service orchestrator that validates input, checks permissions, coordinates multiple services, handles errors, and logs operations. Such a method might legitimately have a cyclomatic complexity of 15 to 20 while being perfectly maintainable if it\u0026rsquo;s well-structured with clear sections and appropriate abstractions. In 2023, I watched a team spend two full weeks refactoring a beautifully clear order processing coordinator, splitting it into 18 micro-methods scattered across four files, all because SonarQube flagged it red. The result? Nobody on the team could follow the execution flow anymore without constantly jumping between files. The cyclomatic complexity went down. The actual maintainability went down with it.\u003c/p\u003e\n\u003cp\u003eConversely, a method with a cyclomatic complexity of 5 could be an absolute maintenance nightmare if it contains obscure bit manipulation, poorly named variables, or nested ternary operators that mask its true intent. I\u0026rsquo;ve seen plenty of \u0026ldquo;low complexity\u0026rdquo; code that nobody dares touch because figuring out what it actually does requires a whiteboard and strong coffee.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe metric is a signal, not a verdict.\u003c/strong\u003e Treating threshold violations as automated refactoring triggers leads to cargo cult programming. You end up splitting methods not because it actually improves design, but because a tool says a number is too high. If you\u0026rsquo;re refactoring solely to satisfy a metric, you\u0026rsquo;re doing it wrong. Full stop.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuring-code-metrics-analysis-in-net\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#configuring-code-metrics-analysis-in-net\" title=\"Configuring Code Metrics Analysis in .NET\"\u003eConfiguring Code Metrics Analysis in .NET\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eDespite these significant limitations, code metrics remain useful when properly configured and (this is crucial) interpreted with actual human judgment. The key is setting them up as discussion triggers, not as automated quality gates. Here\u0026rsquo;s how to configure them effectively without falling into the trap of metric-driven madness.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"enabling-code-metrics-in-visual-studio\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#enabling-code-metrics-in-visual-studio\" title=\"Enabling Code Metrics in Visual Studio\"\u003eEnabling Code Metrics in Visual Studio\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCode metrics can be calculated for entire solutions, projects, or individual code files. Visual Studio makes this relatively straightforward, even if the UI hasn\u0026rsquo;t been meaningfully updated since roughly 2010.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSolution or Project Level\u003c/strong\u003e: Right-click on the solution or project in Solution Explorer, then select \u003cstrong\u003eAnalyze and Code Cleanup\u003c/strong\u003e followed by \u003cstrong\u003eCalculate Code Metrics\u003c/strong\u003e. This gives you a basic overview, though frankly the results window is a masterclass in wasted screen real estate.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eEditorConfig Configuration\u003c/strong\u003e: For more meaningful control, use \u003ccode\u003e.editorconfig\u003c/code\u003e to configure specific metric thresholds:\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Code metrics configuration\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[*.cs]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Cyclomatic complexity warning threshold\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1502.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003ewarning\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1502.cyclomatic_complexity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e25\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Maintainability index warning threshold\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1501.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003ewarning\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1501.maintainability_index\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e15\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Maximum lines of code per method\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1505.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003esuggestion\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1505.lines_of_code\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e100\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"the-ca1509-rule-understanding-invalid-configuration\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#the-ca1509-rule-understanding-invalid-configuration\" title=\"The CA1509 Rule: Understanding Invalid Configuration\"\u003eThe CA1509 Rule: Understanding Invalid Configuration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s documentation on \u003cstrong\u003eCA1509\u003c/strong\u003e addresses a specific but genuinely important issue: \u003cstrong\u003einvalid analyzer configuration values\u003c/strong\u003e. This rule fires when you\u0026rsquo;ve misconfigured code analysis settings, typically by:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eProviding non-numeric values where numbers are expected\u003c/li\u003e\n\u003cli\u003eUsing values outside acceptable ranges\u003c/li\u003e\n\u003cli\u003eSpecifying invalid enumeration values\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eExample of \u003cstrong\u003einvalid configuration\u003c/strong\u003e (that will silently fail in older tooling):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# WRONG: Non-numeric value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1502.cyclomatic_complexity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003ehigh\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# WRONG: Value out of range\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1502.cyclomatic_complexity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e-5\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# WRONG: Invalid severity level\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1502.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003ecritical\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCorrect configuration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# CORRECT: Numeric value in valid range\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1502.cyclomatic_complexity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e25\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# CORRECT: Valid severity level\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1502.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003ewarning\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe CA1509 rule exists precisely because \u003cstrong\u003esilent failures in configuration can lead to dangerously false confidence\u003c/strong\u003e. You might genuinely believe you\u0026rsquo;ve enforced strict complexity limits when, in reality, your misconfiguration has effectively disabled the checks entirely. In a previous role, our team operated for three months under the assumption that our code quality gates were being enforced. They weren\u0026rsquo;t. A single typo (\u003ccode\u003ewarnin\u003c/code\u003e instead of \u003ccode\u003ewarning\u003c/code\u003e) in the \u003ccode\u003e.editorconfig\u003c/code\u003e had neutered the entire setup. Nobody noticed until a code review caught something that should have been flagged automatically.\u003c/p\u003e\n\u003cp\u003eThis is particularly insidious in team environments where configuration files are committed to source control. A typo or misunderstanding propagates across the entire team, systematically undermining code quality initiatives without anyone noticing until much later. Usually someone eventually asks \u0026ldquo;Why isn\u0026rsquo;t this rule triggering?\u0026rdquo; and then you discover that your quality process has been theatre for weeks or months.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"project-level-configuration\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#project-level-configuration\" title=\"Project-Level Configuration\"\u003eProject-Level Configuration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor comprehensive control, configure code metrics in your \u003ccode\u003e.csproj\u003c/code\u003e or \u003ccode\u003eDirectory.Build.props\u003c/code\u003e file:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Enable all code analysis rules --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;EnableNETAnalyzers\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/EnableNETAnalyzers\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisLevel\u0026gt;\u003c/span\u003elatest\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisLevel\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;EnforceCodeStyleInBuild\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/EnforceCodeStyleInBuild\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Treat specific metrics violations as errors in CI/CD --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;WarningsAsErrors\u0026gt;\u003c/span\u003eCA1502;CA1505\u003cspan class=\"nt\"\u003e\u0026lt;/WarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Add code analysis package for additional metrics --\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;Microsoft.CodeAnalysis.NetAnalyzers\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;8.0.0\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\u003eThis configuration ensures that:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCode analysis runs during every build (not just when you remember to click \u0026ldquo;Calculate Metrics\u0026rdquo;)\u003c/li\u003e\n\u003cli\u003eSpecific violations break the build in CI/CD environments (preventing \u0026ldquo;I\u0026rsquo;ll fix it later\u0026rdquo; syndrome)\u003c/li\u003e\n\u003cli\u003eTeams maintain consistent standards across all machines (no more \u0026ldquo;it works on my machine\u0026rdquo; with different analyzer settings)\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"real-world-examples-when-metrics-mislead\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#real-world-examples-when-metrics-mislead\" title=\"Real-World Examples: When Metrics Mislead\"\u003eReal-World Examples: When Metrics Mislead\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere are some actual scenarios from production codebases where metrics painted an incomplete (or outright misleading) picture. These are real examples I\u0026rsquo;ve encountered, though I\u0026rsquo;ve simplified them slightly for clarity.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"example-1-the-high-complexity-coordinator\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#example-1-the-high-complexity-coordinator\" title=\"Example 1: The High-Complexity Coordinator\"\u003eExample 1: The High-Complexity Coordinator\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Cyclomatic Complexity: 18\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_authService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eValidateUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserId\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnauthorized\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInvalid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;No items in order\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003einventory\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_inventoryService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCheckAvailabilityAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003einventory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAllAvailable\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOutOfStock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einventory\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUnavailableItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epayment\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_paymentService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessPaymentAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePayment\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentFailed\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReason\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequiresShipping\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eshipping\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_shippingService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCalculateShippingAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eshipping\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCanDeliver\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eShippingUnavailable\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_orderRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateOrderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003epayment\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einventory\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_notificationService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendOrderConfirmationAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eMetric\u003c/strong\u003e: Cyclomatic complexity of 18\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTool Recommendation\u003c/strong\u003e: Split this method immediately into smaller methods to reduce complexity.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReality\u003c/strong\u003e: This is a well-structured orchestration method with clear, sequential steps. Each condition represents a legitimate business rule. The flow is obvious: validate, check inventory, process payment, arrange shipping, create order. Splitting this would scatter related logic across multiple methods without improving understandability. In fact, it would make it worse because you\u0026rsquo;d need to trace through multiple method calls to understand what should be a single logical operation.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"example-2-the-deceptively-simple-method\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#example-2-the-deceptively-simple-method\" title=\"Example 2: The Deceptively Simple Method\"\u003eExample 2: The Deceptively Simple Method\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculateDiscount\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Cyclomatic Complexity: 3\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTier\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Gold\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"m\"\u003e20\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e           \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTier\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Silver\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e500\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eMetric\u003c/strong\u003e: Cyclomatic complexity of 3\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTool Recommendation\u003c/strong\u003e: Acceptable, no action needed\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReality\u003c/strong\u003e: This nested ternary operator is significantly harder to understand than its complexity suggests. The metric doesn\u0026rsquo;t capture cognitive load. This should be refactored into explicit if/else blocks or (better) a strategy pattern, despite having \u0026ldquo;acceptable\u0026rdquo; metrics. Low complexity doesn\u0026rsquo;t automatically mean readable code.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"example-3-the-configuration-validation\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#example-3-the-configuration-validation\" title=\"Example 3: The Configuration Validation\"\u003eExample 3: The Configuration Validation\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eValidateConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAppConfiguration\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Cyclomatic Complexity: 25\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabaseConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eApiKey\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTimeout\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxRetries\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMaxRetries\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServiceUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"n\"\u003eUri\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServiceUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eUriKind\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAbsolute\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheSize\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCacheSize\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogPath\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWorkers\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e||\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWorkers\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEncryptionKey\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEncryptionKey\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e16\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ... and so on\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eMetric\u003c/strong\u003e: Cyclomatic complexity of 25+\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTool Recommendation\u003c/strong\u003e: CRITICAL! Refactor immediately! Code smell detected!\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReality\u003c/strong\u003e: This is a guard clause pattern performing straightforward validation. Each check is independent and clear. While this could potentially be refactored to use a validation framework like FluentValidation, the current implementation is perfectly maintainable. Anyone can read this method and immediately understand what\u0026rsquo;s being validated. \u003cstrong\u003eThe high complexity reflects the number of configuration options, not poor design.\u003c/strong\u003e Refactoring this just to lower a number would likely make it more complex to understand, not less.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"best-practices-for-code-metrics-configuration\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#best-practices-for-code-metrics-configuration\" title=\"Best Practices for Code Metrics Configuration\"\u003eBest Practices for Code Metrics Configuration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBased on years of working with code metrics across various projects (and watching teams both succeed and fail spectacularly at using them), here are practical recommendations:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"1-set-contextual-thresholds\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#1-set-contextual-thresholds\" title=\"1. Set Contextual Thresholds\"\u003e1. Set Contextual Thresholds\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDon\u0026rsquo;t use Microsoft\u0026rsquo;s default thresholds blindly. They were probably set by someone who never maintained a real-world enterprise application. Analyze your actual codebase and set realistic limits based on what you\u0026rsquo;re building:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[*.cs]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# More lenient for coordinators and facades\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1502.cyclomatic_complexity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e25\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Stricter for business logic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[**/Domain/**.cs]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_code_quality.CA1502.cyclomatic_complexity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e15\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Most lenient for generated code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[**/Generated/**.cs]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003edotnet_diagnostic.CA1502.severity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003enone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"2-combine-metrics-with-code-reviews\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#2-combine-metrics-with-code-reviews\" title=\"2. Combine Metrics with Code Reviews\"\u003e2. Combine Metrics with Code Reviews\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMetrics are \u003cstrong\u003eindicators for discussion\u003c/strong\u003e, not automatic refactoring triggers. This cannot be emphasized enough. When a metric threshold is exceeded:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eReview the code with the team (not just accept what the tool says)\u003c/li\u003e\n\u003cli\u003eDiscuss whether the complexity is essential or accidental\u003c/li\u003e\n\u003cli\u003eConsider readability and maintainability alongside the numbers\u003c/li\u003e\n\u003cli\u003eRefactor only when there\u0026rsquo;s genuine consensus that it improves the code\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eIf someone suggests refactoring purely because \u0026ldquo;the metric is red,\u0026rdquo; push back. Ask them to explain what specifically is hard to understand or maintain. If they can\u0026rsquo;t articulate a concrete problem beyond \u0026ldquo;the number is high,\u0026rdquo; the refactoring probably isn\u0026rsquo;t worth doing.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"3-track-trends-not-absolutes\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#3-track-trends-not-absolutes\" title=\"3. Track Trends, Not Absolutes\"\u003e3. Track Trends, Not Absolutes\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFocus on whether metrics are improving or degrading over time, not on hitting specific numbers. A codebase where average complexity is gradually decreasing is healthier than one where you\u0026rsquo;ve arbitrarily set thresholds that everyone routinely suppresses:\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=\"c\"\u003e\u0026lt;!-- Ratchet approach: prevent regression --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;MaxAllowedCyclomaticComplexity\u0026gt;\u003c/span\u003e25\u003cspan class=\"nt\"\u003e\u0026lt;/MaxAllowedCyclomaticComplexity\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Gradually decrease this threshold over time --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"4-validate-your-configuration\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#4-validate-your-configuration\" title=\"4. Validate Your Configuration\"\u003e4. Validate Your Configuration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEnsure your \u003ccode\u003e.editorconfig\u003c/code\u003e and project settings are actually valid and doing what you think they\u0026rsquo;re doing:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Build with warnings as errors to catch configuration issues\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet build /p:TreatWarningsAsErrors\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis will cause CA1509 violations (invalid configuration) to break the build, preventing those silent failures that undermine your entire quality process.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"5-document-exceptions\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#5-document-exceptions\" title=\"5. Document Exceptions\"\u003e5. Document Exceptions\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen you intentionally exceed thresholds (and you will), document why. Future developers (including yourself in six months) will thank you:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Cyclomatic complexity: 22\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// JUSTIFICATION: This coordinator method orchestrates the entire checkout process.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Splitting it would scatter cohesive logic and reduce maintainability.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Reviewed: 2024-12-15, approved by architecture team.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[SuppressMessage(\u0026#34;Microsoft.Maintainability\u0026#34;, \u0026#34;CA1502:AvoidExcessiveComplexity\u0026#34;,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    Justification = \u0026#34;Orchestration method with clear sequential steps\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eCheckoutResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessCheckoutAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCheckoutRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Implementation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch2 id=\"the-bigger-picture-metrics-as-tools-not-goals\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#the-bigger-picture-metrics-as-tools-not-goals\" title=\"The Bigger Picture: Metrics as Tools, Not Goals\"\u003eThe Bigger Picture: Metrics as Tools, Not Goals\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe fundamental issue with code metrics isn\u0026rsquo;t the measurements themselves. It\u0026rsquo;s how we respond to them. \u003cstrong\u003eOptimizing for metrics rather than maintainability is a textbook case of Goodhart\u0026rsquo;s Law\u003c/strong\u003e: \u0026ldquo;When a measure becomes a target, it ceases to be a good measure.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched teams obsess over reducing cyclomatic complexity, creating elaborate abstractions and indirection that make the code objectively harder to understand, all to satisfy a tool\u0026rsquo;s threshold. I\u0026rsquo;ve seen developers split perfectly cohesive methods into multiple smaller methods that require jumping between five different files to understand the flow, all because a metric said \u0026ldquo;this is too complex.\u0026rdquo; The metric improved. The code got worse. Nobody seemed to notice the contradiction.\u003c/p\u003e\n\u003cp\u003eIn one particularly memorable case, a developer created a 200-line method that consisted entirely of calls to other private methods in the same class. Each of those methods was 10 to 15 lines and accessed the same five instance fields. The cyclomatic complexity of each individual method was beautiful. The overall design was a maintenance nightmare. But hey, the metrics dashboard was green, so management was happy.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCode metrics should inform judgment, not replace it.\u003c/strong\u003e They highlight areas that deserve scrutiny, but the decision to refactor must be based on whether the change genuinely improves the codebase. If you find yourself refactoring code that was already clear and maintainable just to lower a number, stop. You\u0026rsquo;re making things worse while convincing yourself you\u0026rsquo;re making them better.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuration-checklist-for-production-systems\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#configuration-checklist-for-production-systems\" title=\"Configuration Checklist for Production Systems\"\u003eConfiguration Checklist for Production Systems\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhen setting up code metrics for a production system, ensure you:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEnable all relevant analyzers\u003c/strong\u003e via \u003ccode\u003eEnableNETAnalyzers\u003c/code\u003e and \u003ccode\u003eAnalysisLevel\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConfigure thresholds based on your codebase\u003c/strong\u003e, not arbitrary industry standards\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eValidate configuration\u003c/strong\u003e by treating warnings as errors during CI/CD builds\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDocument your thresholds and rationale\u003c/strong\u003e in a \u003ccode\u003eCODE_METRICS.md\u003c/code\u003e file\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReview violations as a team\u003c/strong\u003e before enforcing automatic build failures\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCreate exemptions for special cases\u003c/strong\u003e (generated code, third-party code, test data builders)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMonitor trends over time\u003c/strong\u003e rather than focusing on absolute values\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntegrate with pull request checks\u003c/strong\u003e to catch regressions early\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProvide training\u003c/strong\u003e to help developers understand what the metrics mean\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRevisit thresholds regularly\u003c/strong\u003e as the codebase and team mature\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts-measure-what-matters\"\u003e\u003ca href=\"/posts/code-metrics-configuration/#final-thoughts-measure-what-matters\" title=\"Final Thoughts: Measure What Matters\"\u003eFinal Thoughts: Measure What Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCode metrics are valuable tools when used with appropriate skepticism and contextual awareness. They can highlight potential issues, guide code reviews, and track quality trends over time. But they are \u003cstrong\u003ediagnostic tools, not prescriptive rules\u003c/strong\u003e. They tell you where to look, not what to do.\u003c/p\u003e\n\u003cp\u003eThe CA1509 rule\u0026rsquo;s existence (a rule about configuring rules correctly) is almost poetic in its meta-commentary on the complexity of modern code analysis. We\u0026rsquo;ve built elaborate systems to measure code quality, then needed additional rules to ensure we\u0026rsquo;re measuring correctly, all while the fundamental question remains: \u003cstrong\u003eAre we building software that solves real problems effectively?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eConfigure your code metrics thoughtfully. Understand what they measure and (equally important) what they don\u0026rsquo;t measure. Use them to start conversations, not end them. And above all, \u003cstrong\u003eremember that the goal is maintainable, working software, not perfectly scoring metrics.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eBecause at the end of the day, your users don\u0026rsquo;t care about your cyclomatic complexity. They care whether your software works reliably, performs well, and can be enhanced to meet their evolving needs. Code metrics can help you achieve that, but only if you resist the temptation to treat them as the goal itself.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re a developer who\u0026rsquo;s been blindly following metric thresholds, consider this your wake-up call. If you\u0026rsquo;re a team lead enforcing arbitrary complexity limits without understanding context, you\u0026rsquo;re actively harming your codebase while congratulating yourself on maintaining \u0026ldquo;quality standards.\u0026rdquo; The numbers matter, but they don\u0026rsquo;t matter more than clear, maintainable code that actually works.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-18T17:00:00+01:00","id":"https://daily-devops.net/posts/code-metrics-configuration/","language":"en","summary":"A critical look at .NET and Visual Studio code metrics, their configuration, and why context matters infinitely more than arbitrary thresholds.","tags":["codequality","bestpractices","csharp","dotnet","visualstudio","softwareengineering"],"title":"Code Metrics and Configuration: Beyond the Numbers Game","url":"https://daily-devops.net/posts/code-metrics-configuration/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eEach November, the same pattern unfolds.\nMicrosoft releases a new runtime, SDK, and language update. Documentation floods in. Build agents are reconfigured. Teams pause to ask the same question: \u003cem\u003eIs now the right moment to migrate?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThis cadence is comforting in its consistency — and quietly draining in its demands.\nIt offers the illusion of control, yet it forces constant motion.\nThat is the \u003cstrong\u003erelease cycle paradox\u003c/strong\u003e: the same predictability that simplifies planning also accelerates fatigue.\u003c/p\u003e\n\u003cp\u003e.NET 10 exemplifies this tension. The improvements are genuine — tighter runtime optimizations, richer AOT support, and smarter trimming — but they’re cumulative. Every skipped version multiplies effort. Predictable progress punishes hesitation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"predictability-as-pressure\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#predictability-as-pressure\" title=\"Predictability as Pressure\"\u003ePredictability as Pressure\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhat once made .NET dependable has now made it demanding.\nThe community can predict the exact week of a new release, plan migrations, and even pre-test their pipelines — yet many still find themselves unprepared when the SDK ships.\u003c/p\u003e\n\u003cp\u003ePredictability is not peace of mind.\nIt creates a quiet, steady form of pressure — the kind that rewards discipline and punishes delay.\nThe calendar doesn’t care about your backlog. The cadence continues whether you’re ready or not.\u003c/p\u003e\n\u003cp\u003eA quick real-world note from our side: last November we had the date pinned in the team calendar and still opened Monday to a wall of red builds. One Linux agent image hadn’t pulled the expected workloads; the global.json was right. The agent wasn’t. The fix took 15 minutes once identified (pin SDK + run workload restore in CI), but the surprise cost us a sprint’s focus that week. Since then, we do a short “SDK drift” check before release week.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-technical-face-of-the-paradox\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#the-technical-face-of-the-paradox\" title=\"The Technical Face of the Paradox\"\u003eThe Technical Face of the Paradox\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"c-14--predictable-refinement-unpredictable-friction\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#c-14--predictable-refinement-unpredictable-friction\" title=\"C# 14 — Predictable Refinement, Unpredictable Friction\"\u003eC# 14 — Predictable Refinement, Unpredictable Friction\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eC# 14 continues the trend toward subtle, precision-driven changes. Inline \u003ccode\u003eparams\u003c/code\u003e, improved pattern matching, and more expressive interpolated strings create cleaner, safer code.\nBut compiler strictness has increased; nullable and diagnostic warnings appear in code that once passed quietly.\u003c/p\u003e\n\u003cp\u003eEach small improvement adds friction — not because the platform is unstable, but because it evolves exactly as promised.\nPredictability creates work, and that’s the paradox in action.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"runtime--sdk-discipline\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#runtime--sdk-discipline\" title=\"Runtime \u0026amp; SDK Discipline\"\u003eRuntime \u0026amp; SDK Discipline\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 10’s runtime is sharper than ever: smarter tiered compilation, more consistent JIT heuristics, and finally, production-grade Native AOT.\nYet these advancements force teams to reevaluate long-standing practices — reflection-heavy logic, late-bound dependencies, or dynamic configuration.\u003c/p\u003e\n\u003cp\u003eThe SDK, too, has matured into something stricter.\nWorkload isolation improves reliability, but CI/CD pipelines must now be explicit about everything — from installed workloads to exact SDK versions.\nPredictable upgrades require constant vigilance.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe AOT issues we hit didn’t come from application code; they came from a build assumption we hadn’t questioned in years.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eLog excerpt from a pipeline that failed during a trial upgrade:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-txt\" data-lang=\"txt\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eerror NETSDK1147: Workload manifest not found for \u0026#39;wasm-tools\u0026#39;.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEnsure the workload is installed or pin the SDK version.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch2 id=\"open-source-and-the-rhythm-of-agility\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#open-source-and-the-rhythm-of-agility\" title=\"Open Source and the Rhythm of Agility\"\u003eOpen Source and the Rhythm of Agility\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIn the open-source ecosystem, this rhythm works beautifully.\nMaintainers expect the November cycle, align their releases, and upgrade within days. Early adoption becomes a badge of discipline — a signal of trust and maturity.\u003c/p\u003e\n\u003cp\u003eThe same cycle that pressures enterprises empowers open source.\nIts predictability encourages contribution and iteration. When Microsoft releases .NET 10, the community is already ready.\u003c/p\u003e\n\u003cp\u003eFor enterprise software, that luxury rarely exists.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"enterprise-reality-predictability-without-readiness\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#enterprise-reality-predictability-without-readiness\" title=\"Enterprise Reality: Predictability Without Readiness\"\u003eEnterprise Reality: Predictability Without Readiness\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCorporate software teams love predictability — in theory.\nIn practice, the yearly rhythm exposes their organizational inertia. Every release date is known far in advance, yet migrations still arrive as “urgent surprises.”\u003c/p\u003e\n\u003cp\u003eThis is where the paradox becomes visible in the calendar itself.\nReleases arrive on schedule; readiness never does.\nBy the time migration planning begins, dependencies have drifted, internal frameworks have frozen, and the “safe delay” has grown into a full technical backlog.\u003c/p\u003e\n\u003cp\u003ePredictable innovation meets unpredictable culture.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migration-as-a-habit-not-an-event\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#migration-as-a-habit-not-an-event\" title=\"Migration as a Habit, Not an Event\"\u003eMigration as a Habit, Not an Event\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMigration should never feel like a special occasion.\nThe most successful .NET teams have learned that upgrading is not a project — it’s a rhythm, a continuous engineering motion that’s as natural as code review or CI automation.\u003c/p\u003e\n\u003cp\u003eThey don’t treat .NET 10 as a milestone to fear, but as another iteration in a living system.\nEach month, build agents get refreshed, SDKs are validated, dependencies checked, analyzers tuned. Not because it’s urgent — but because it’s \u003cem\u003enormal\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003eThat’s what separates organizations that evolve gracefully from those that collapse under “surprise migrations.”\nThe difference isn’t budget or tooling; it’s culture.\u003c/p\u003e\n\u003cp\u003eA team that lives in rhythm with the release cycle never faces “big bang” upgrades.\nTheir codebase stays modern almost by accident. The build pipelines already support multiple SDKs, the CI agents are modular, and new features like Native AOT or improved analyzers don’t require a six-month “initiative.” They just appear, because the system expects change.\u003c/p\u003e\n\u003cp\u003eTo build this culture, start small:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eInstitutionalize curiosity.\u003c/strong\u003e Let developers explore new SDKs as part of regular work, not as weekend experiments.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAutomate awareness.\u003c/strong\u003e Make SDK updates, package audits, and analyzer warnings visible in your CI pipeline outputs. Visibility creates momentum.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePlan migrations in sprints, not quarters.\u003c/strong\u003e Each upgrade should fit inside your delivery rhythm, not break it.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEmpower ownership.\u003c/strong\u003e Assign “modernization owners” — developers who drive awareness, collect upgrade blockers, and keep the team fluent in the current runtime.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOver time, the organization stops thinking of migration as a cost center and begins to recognize it as an investment in velocity.\nEvery release, from .NET 8 to .NET 10 and beyond, becomes a calibration point rather than a disruption.\u003c/p\u003e\n\u003cp\u003eContinuous migration isn’t glamorous, but it’s the quiet discipline that separates engineering teams that \u003cem\u003emove\u003c/em\u003e from those that \u003cem\u003emaintain\u003c/em\u003e.\nAnd in an ecosystem where predictability never pauses, habit is the only sustainable strategy.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"practical-snippets-what-we-actually-changed\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#practical-snippets-what-we-actually-changed\" title=\"Practical snippets (what we actually changed)\"\u003ePractical snippets (what we actually changed)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePin the SDK and keep roll-forward predictable via \u003ccode\u003eglobal.json\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;sdk\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;version\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;10.0.100\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;rollForward\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;latestFeature\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eMake workloads explicit in CI (example with GitHub Actions):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eSetup .NET\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003euses\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eactions/setup-dotnet@v4\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003ewith\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003edotnet-version\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;10.0.x\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eRestore workloads\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003edotnet workload restore\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eHarden code for trimming/AOT by avoiding reflection where possible; when unavoidable, preserve types deliberately:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Prefer explicit registrations or source generators over late-bound reflection\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eservices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddSingleton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIReportFormatter\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eJsonReportFormatter\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// If reflection is required, preserve members for AOT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(JsonReportFormatter))]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003ePreserveForAot\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRaise the bar on diagnostics consistently across projects:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;TreatWarningsAsErrors\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/TreatWarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;AnalysisLevel\u0026gt;\u003c/span\u003elatest\u003cspan class=\"nt\"\u003e\u0026lt;/AnalysisLevel\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;WarningsAsErrors\u0026gt;\u003c/span\u003enullable;CS8600;CS8618\u003cspan class=\"nt\"\u003e\u0026lt;/WarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Keep build times honest; tune if CI exceeds your threshold --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch2 id=\"make-the-release-cycle-work-for-you\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#make-the-release-cycle-work-for-you\" title=\"Make the Release Cycle Work for You\"\u003eMake the Release Cycle Work for You\u003c/a\u003e\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eBuild rhythm into your engineering culture — before the next cycle arrives.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003eValidate SDKs monthly to keep pipelines evergreen. (Adjust timing based on your codebase size and team needs.) We book a 25‑minute “SDK drift” slot on the first Tuesday: check \u003ccode\u003eglobal.json\u003c/code\u003e, patch the agent image (e.g., Ubuntu 22.04), and compare \u003ccode\u003edotnet workload list\u003c/code\u003e against 10.0.x.\u003c/li\u003e\n\u003cli\u003eAudit dependencies quarterly to avoid silent decay. (Session length and CI thresholds should be tuned for your project.) Cap the session to 2 hours; if CI time grows \u0026gt;10 minutes from analyzer changes, stop and adjust.\u003c/li\u003e\n\u003cli\u003eTrain developers on upcoming .NET features early, before release. Short brown‑bag demos beat slide decks.\u003c/li\u003e\n\u003cli\u003eTreat modernization as automation — continuous, measurable, expected. Track “Mean Time to Upgrade” (e.g., 2.5 person‑days 9→10) and CI fail rate deltas after analyzer tuning.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOur routine, with a few guardrails:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eMonday 10:00 Slack reminder to run \u003ccode\u003edotnet --info\u003c/code\u003e on agents and local dev boxes.\u003c/li\u003e\n\u003cli\u003eWednesday 16:00 update the analyzer “breakers” list and suppress only noisy rules.\u003c/li\u003e\n\u003cli\u003eNo release‑freeze lifting on Fridays after 14:00.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"when-not-to-upgrade-immediately\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#when-not-to-upgrade-immediately\" title=\"When not to upgrade immediately\"\u003eWhen not to upgrade immediately\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThere are sensible reasons to pause — briefly and deliberately:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVendor SDK certifications lagging by 4–6 weeks; upgrade next “safe window”.\u003c/li\u003e\n\u003cli\u003eAudit/compliance blackout windows.\u003c/li\u003e\n\u003cli\u003eMajor product releases where risk must be near zero (freeze applies to infra and SDKs).\u003c/li\u003e\n\u003cli\u003eCritical dependency without .NET 10 support yet; backport security fixes and pin via \u003ccode\u003eglobal.json\u003c/code\u003e until ready.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe key is to time‑box the delay, document the reason, and schedule the follow‑up.\u003c/p\u003e\n\u003cp\u003eSmall routines build resilience.\nThat’s how predictable releases stop being a burden and become a competitive advantage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/dotnet-10-release-cycle-paradox/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003e.NET Release Cycle Paradox\u003c/strong\u003e isn’t a flaw — it’s a reflection of engineering maturity.\nPredictability doesn’t slow us down; resistance does.\nThe teams that embrace rhythm, practice continuous modernization, and turn migration into muscle memory are the ones that thrive — release after release.\u003c/p\u003e\n\u003cp\u003eThe release cycle will keep ticking. The question is no longer \u003cem\u003ewhen\u003c/em\u003e to migrate. It’s whether your organization has learned to move in time with the beat.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-11T16:30:00+01:00","id":"https://daily-devops.net/posts/dotnet-10-release-cycle-paradox/","language":"en","summary":".NET's predictable yearly cadence delivers stability and pressure at once: migration insights, cultural notes, and recommendations for .NET 10.\n","tags":["bestpractices","dotnet","csharp","architecture","softwareengineering"],"title":".NET 10 and the Release Cycle Paradox","url":"https://daily-devops.net/posts/dotnet-10-release-cycle-paradox/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLast month, I watched a senior developer spend three days debugging a build failure that worked perfectly on his machine. The CI pipeline? Failed every single time. Different error messages. Inconsistent behavior. Pure chaos.\u003c/p\u003e\n\u003cp\u003eThe root cause? A single line in a \u003ccode\u003e.csproj\u003c/code\u003e file:\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;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(TargetFramework)\u0026#39; == \u0026#39;net8.0\u0026#39;\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eThat\u0026rsquo;s it.\u003c/strong\u003e One innocent-looking string comparison brought a multi-targeting .NET project to its knees.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what nobody tells you about TargetFramework conditions: string comparisons are a trap. They work on your machine because you\u0026rsquo;re building \u003ccode\u003enet8.0\u003c/code\u003e exactly. They fail in CI because your pipeline builds \u003ccode\u003enet8.0-windows\u003c/code\u003e. They explode in production when someone adds \u003ccode\u003enet8.0-android\u003c/code\u003e six months later. And the worst part? \u003cstrong\u003eThe failures are silent.\u003c/strong\u003e No exceptions. No obvious errors. Just conditions that stop matching and features that mysteriously vanish.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve seen this pattern destroy three separate projects. Multi-targeting nightmares. Build configs that work by accident. Hours of debugging that could have been avoided with \u003cstrong\u003eone single property function\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eMicrosoft documented \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/msbuild/property-functions?view=vs-2022#TargetFramework\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eTargetFramework property functions\u003c/a\u003e years ago, yet developers keep writing fragile string comparisons. So let me be brutally clear: \u003cstrong\u003eif you\u0026rsquo;re using \u003ccode\u003e$(TargetFramework)' == 'something'\u003c/code\u003e conditions, you\u0026rsquo;re sitting on a time bomb.\u003c/strong\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-string-comparisons-fail-silently\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#the-problem-string-comparisons-fail-silently\" title=\"The Problem: String Comparisons Fail Silently\"\u003eThe Problem: String Comparisons Fail Silently\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou know what\u0026rsquo;s worse than a build that fails loudly? A build that fails \u003cstrong\u003equietly\u003c/strong\u003e. String-based TargetFramework conditions don\u0026rsquo;t throw errors. They just stop working. Your feature flags vanish. Your package references disappear. Your platform-specific code never compiles.\u003c/p\u003e\n\u003cp\u003eAnd you won\u0026rsquo;t know until production.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the pattern I see everywhere:\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;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(TargetFramework)\u0026#39; == \u0026#39;net8.0\u0026#39;\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;DefineConstants\u0026gt;\u003c/span\u003e$(DefineConstants);NET8_0\u003cspan class=\"nt\"\u003e\u0026lt;/DefineConstants\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eLooks harmless, right? Simple. Readable. Clean.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIt\u0026rsquo;s a disaster waiting to happen.\u003c/strong\u003e\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-breaks-and-why-you-havent-noticed-yet\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#why-this-breaks-and-why-you-havent-noticed-yet\" title=\"Why This Breaks (And Why You Haven\u0026rsquo;t Noticed Yet)\"\u003eWhy This Breaks (And Why You Haven\u0026rsquo;t Noticed Yet)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTargetFramework isn\u0026rsquo;t just a string. It\u0026rsquo;s a \u003cstrong\u003esemantic identifier\u003c/strong\u003e that MSBuild needs to interpret, not just match character-by-character.\u003c/p\u003e\n\u003cp\u003eWhen you multi-target, MSBuild evaluates your project file multiple times—once per framework. During each pass, \u003ccode\u003e$(TargetFramework)\u003c/code\u003e contains the current framework being built. That part works fine.\u003c/p\u003e\n\u003cp\u003eThe problem shows up when you expand your targeting. Consider this scenario—you\u0026rsquo;re targeting both \u003ccode\u003enet6.0\u003c/code\u003e and \u003ccode\u003enet8.0\u003c/code\u003e:\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;Project\u003c/span\u003e \u003cspan class=\"na\"\u003eSdk=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Microsoft.NET.Sdk\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;TargetFrameworks\u0026gt;\u003c/span\u003enet6.0;net8.0\u003cspan class=\"nt\"\u003e\u0026lt;/TargetFrameworks\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(TargetFramework)\u0026#39; == \u0026#39;net8.0\u0026#39;\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;LangVersion\u0026gt;\u003c/span\u003e12.0\u003cspan class=\"nt\"\u003e\u0026lt;/LangVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/Project\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eToday?\u003c/strong\u003e This works. Your \u003ccode\u003enet8.0\u003c/code\u003e build gets C# 12 features. Great.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTomorrow?\u003c/strong\u003e Product requirements change. You need Windows-specific features. You update to \u003ccode\u003enet8.0-windows\u003c/code\u003e. Suddenly, your condition stops matching. Why? Because \u003ccode\u003e'net8.0-windows' == 'net8.0'\u003c/code\u003e evaluates to \u003ccode\u003efalse\u003c/code\u003e. Obviously. String comparison. Exact match required.\u003c/p\u003e\n\u003cp\u003eYour C# 12 features? Gone. No error. No warning. Just \u003cstrong\u003esilent failure\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the breakdown of what actually goes wrong:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eBrittle exact matching\u003c/strong\u003e: The condition only triggers for \u003ccode\u003enet8.0\u003c/code\u003e precisely. Add any platform specifier—\u003ccode\u003enet8.0-windows\u003c/code\u003e, \u003ccode\u003enet8.0-android\u003c/code\u003e, \u003ccode\u003enet8.0-ios\u003c/code\u003e—and the match fails. Your carefully crafted configuration? Ignored.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eVersion comparisons don\u0026rsquo;t work\u003c/strong\u003e: Try expressing \u0026ldquo;all .NET 8.0 or higher\u0026rdquo; with string comparisons. Go ahead, I\u0026rsquo;ll wait. You end up with nightmare chains of \u003ccode\u003eOR\u003c/code\u003e conditions or messy \u003ccode\u003eContains()\u003c/code\u003e hacks that break on edge cases.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eNo semantic understanding\u003c/strong\u003e: String comparisons have zero awareness of framework relationships. They can\u0026rsquo;t tell that \u003ccode\u003enet8.0\u003c/code\u003e and \u003ccode\u003enet8.0-windows\u003c/code\u003e are related. They can\u0026rsquo;t distinguish .NET Framework from .NET Core from modern .NET. Every edge case requires another manual condition.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eAnd here\u0026rsquo;s the real kicker: \u003cstrong\u003ethis scales horribly\u003c/strong\u003e. Start with one framework. Add a second. Add platform variants. Add legacy .NET Standard support. Suddenly, you have a tangled web of string comparisons that nobody understands and everyone\u0026rsquo;s afraid to touch.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve debugged this exact scenario four times in the last year. Four different teams. Four different projects. Same root cause every single time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-solution-targetframework-property-functions-finally\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#the-solution-targetframework-property-functions-finally\" title=\"The Solution: TargetFramework Property Functions (Finally)\"\u003eThe Solution: TargetFramework Property Functions (Finally)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft didn\u0026rsquo;t leave us hanging. They built proper tooling for this exact problem. It\u0026rsquo;s been in MSBuild for years. Most developers just don\u0026rsquo;t know it exists.\u003c/p\u003e\n\u003cp\u003eEnter \u003cstrong\u003e\u003ccode\u003eIsTargetFrameworkCompatible()\u003c/code\u003e\u003c/strong\u003e—the property function that understands framework semantics instead of just comparing strings like a caveman.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"istargetframeworkcompatible--your-new-best-friend\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#istargetframeworkcompatible--your-new-best-friend\" title=\"IsTargetFrameworkCompatible() — Your New Best Friend\"\u003eIsTargetFrameworkCompatible() — Your New Best Friend\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s the same condition, done correctly:\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;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;net8.0\u0026#39;))\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;LangVersion\u0026gt;\u003c/span\u003e12.0\u003cspan class=\"nt\"\u003e\u0026lt;/LangVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eLooks similar. Behaves \u003cstrong\u003ecompletely differently\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThis function takes two parameters:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eFirst parameter\u003c/strong\u003e: The target framework to check (usually \u003ccode\u003e$(TargetFramework)\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecond parameter\u003c/strong\u003e: The framework moniker to compare against\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBut here\u0026rsquo;s what makes it powerful—it doesn\u0026rsquo;t just match strings. It \u003cstrong\u003eunderstands framework relationships\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eVersion awareness\u003c/strong\u003e: It knows \u003ccode\u003enet9.0\u003c/code\u003e is compatible with \u003ccode\u003enet8.0\u003c/code\u003e requirements, but \u003ccode\u003enet7.0\u003c/code\u003e isn\u0026rsquo;t. Try doing \u003cem\u003ethat\u003c/em\u003e with string equality.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePlatform-specific intelligence\u003c/strong\u003e: Both \u003ccode\u003enet8.0\u003c/code\u003e and \u003ccode\u003enet8.0-windows\u003c/code\u003e correctly match against \u003ccode\u003enet8.0\u003c/code\u003e. No more silent failures when you add platform specifiers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFramework family understanding\u003c/strong\u003e: It handles .NET Framework vs. .NET Core vs. modern .NET semantics. It knows the compatibility matrix. You don\u0026rsquo;t have to.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is the difference between \u003cstrong\u003epattern matching\u003c/strong\u003e and \u003cstrong\u003esemantic understanding\u003c/strong\u003e. One is fragile. The other actually works.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"real-world-scenarios-where-this-saved-my-ass\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#real-world-scenarios-where-this-saved-my-ass\" title=\"Real-World Scenarios (Where This Saved My Ass)\"\u003eReal-World Scenarios (Where This Saved My Ass)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLet me show you where this matters in actual production code:\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"scenario-1-conditional-package-references\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#scenario-1-conditional-package-references\" title=\"Scenario 1: Conditional Package References\"\u003eScenario 1: Conditional Package References\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003eYou\u0026rsquo;re using a modern testing library that only exists for .NET 8+:\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\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;net8.0\u0026#39;))\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;Microsoft.Extensions.TimeProvider.Testing\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;8.11.0\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\u003eThis condition ensures the package references correctly for:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003enet8.0\u003c/code\u003e ✅\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enet8.0-windows\u003c/code\u003e ✅\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enet9.0\u003c/code\u003e ✅\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enet7.0\u003c/code\u003e ❌ (correctly excluded)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWith string comparison? You\u0026rsquo;d need four separate conditions. And you\u0026rsquo;d still miss edge cases.\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"scenario-2-platform-specific-features\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#scenario-2-platform-specific-features\" title=\"Scenario 2: Platform-Specific Features\"\u003eScenario 2: Platform-Specific Features\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003eWindows desktop app with conditional WPF support:\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;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;net8.0-windows\u0026#39;))\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;UseWPF\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/UseWPF\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;UseWindowsForms\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/UseWindowsForms\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis activates \u003cstrong\u003eonly\u003c/strong\u003e for Windows-specific .NET 8.0+ builds. Cross-platform \u003ccode\u003enet8.0\u003c/code\u003e targets? Correctly ignored. No WPF dragged into your Linux containers by accident.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve seen production deployments break because someone enabled WPF on a cross-platform build. This pattern prevents that entirely.\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"scenario-3-legacy-framework-support-the-painful-one\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#scenario-3-legacy-framework-support-the-painful-one\" title=\"Scenario 3: Legacy Framework Support (The Painful One)\"\u003eScenario 3: Legacy Framework Support (The Painful One)\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003eYou\u0026rsquo;re maintaining a library that still targets .NET Standard 2.0 for broad compatibility:\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\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;netstandard2.0\u0026#39;))\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;System.Text.Json\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;8.0.5\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.NET Standard 2.0 needs explicit package references for APIs that are built-in on modern .NET. This condition ensures:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003enetstandard2.0\u003c/code\u003e gets the package ✅\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enet6.0\u003c/code\u003e, \u003ccode\u003enet8.0\u003c/code\u003e don\u0026rsquo;t need it ✅ (already in the framework)\u003c/li\u003e\n\u003cli\u003eNo duplicate references ✅\u003c/li\u003e\n\u003cli\u003eNo manual version matrix management ✅\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis is the kind of problem that causes subtle runtime failures if you get it wrong. String comparisons can\u0026rsquo;t express this logic cleanly.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"additional-framework-functions-when-you-need-fine-control\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#additional-framework-functions-when-you-need-fine-control\" title=\"Additional Framework Functions (When You Need Fine Control)\"\u003eAdditional Framework Functions (When You Need Fine Control)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eIsTargetFrameworkCompatible()\u003c/code\u003e solves 95% of use cases. But sometimes, you need more granular control. Microsoft provides helper functions for extracting specific framework details:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"gettargetframeworkidentifier\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#gettargetframeworkidentifier\" title=\"GetTargetFrameworkIdentifier()\"\u003eGetTargetFrameworkIdentifier()\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eExtracts just the framework identifier:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Returns \u0026#34;.NETCoreApp\u0026#34; for net8.0 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;FrameworkId\u0026gt;\u003c/span\u003e$([MSBuild]::GetTargetFrameworkIdentifier(\u0026#39;$(TargetFramework)\u0026#39;))\u003cspan class=\"nt\"\u003e\u0026lt;/FrameworkId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eUseful when you need to distinguish .NET Core from .NET Framework from .NET Standard, but don\u0026rsquo;t care about versions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"gettargetframeworkversion\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#gettargetframeworkversion\" title=\"GetTargetFrameworkVersion()\"\u003eGetTargetFrameworkVersion()\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eExtracts just the version:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Returns \u0026#34;8.0\u0026#34; for net8.0 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;FrameworkVer\u0026gt;\u003c/span\u003e$([MSBuild]::GetTargetFrameworkVersion(\u0026#39;$(TargetFramework)\u0026#39;))\u003cspan class=\"nt\"\u003e\u0026lt;/FrameworkVer\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eHandy for version-specific logic where framework family doesn\u0026rsquo;t matter.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"gettargetplatformidentifier-and-gettargetplatformversion\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#gettargetplatformidentifier-and-gettargetplatformversion\" title=\"GetTargetPlatformIdentifier() and GetTargetPlatformVersion()\"\u003eGetTargetPlatformIdentifier() and GetTargetPlatformVersion()\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor platform-specific targeting (Windows, Android, iOS, etc.):\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Returns \u0026#34;windows\u0026#34; for net8.0-windows10.0.19041.0 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;PlatformId\u0026gt;\u003c/span\u003e$([MSBuild]::GetTargetPlatformIdentifier(\u0026#39;$(TargetFramework)\u0026#39;))\u003cspan class=\"nt\"\u003e\u0026lt;/PlatformId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Returns \u0026#34;10.0.19041.0\u0026#34; for net8.0-windows10.0.19041.0 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;PlatformVer\u0026gt;\u003c/span\u003e$([MSBuild]::GetTargetPlatformVersion(\u0026#39;$(TargetFramework)\u0026#39;))\u003cspan class=\"nt\"\u003e\u0026lt;/PlatformVer\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThese become critical when you\u0026rsquo;re building cross-platform apps with platform-specific features. No more parsing strings manually. No more regex hacks. Just clean extraction of the data you need.\u003c/p\u003e\n\u003cp\u003eI rarely need these helper functions, honestly. \u003ccode\u003eIsTargetFrameworkCompatible()\u003c/code\u003e handles most scenarios. But when you\u0026rsquo;re dealing with complex multi-platform builds (looking at you, MAUI projects), these become indispensable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"common-mistakes-that-ive-made-too\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#common-mistakes-that-ive-made-too\" title=\"Common Mistakes (That I\u0026rsquo;ve Made Too)\"\u003eCommon Mistakes (That I\u0026rsquo;ve Made Too)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEven when you know about these functions, it\u0026rsquo;s easy to screw them up. Here are the mistakes I see most often—and yes, I\u0026rsquo;ve made every single one of these myself:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"mistake-1-inverting-the-compatibility-check\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#mistake-1-inverting-the-compatibility-check\" title=\"Mistake #1: Inverting the Compatibility Check\"\u003eMistake #1: Inverting the Compatibility Check\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis is the most common error, and it\u0026rsquo;s \u003cstrong\u003esubtle\u003c/strong\u003e:\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=\"c\"\u003e\u0026lt;!-- WRONG: Parameters reversed --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;net8.0\u0026#39;, \u0026#39;$(TargetFramework)\u0026#39;))\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;LangVersion\u0026gt;\u003c/span\u003e12.0\u003cspan class=\"nt\"\u003e\u0026lt;/LangVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSee the problem? The parameters are backwards. This checks if \u003ccode\u003enet8.0\u003c/code\u003e is compatible with your target, not if your target is compatible with \u003ccode\u003enet8.0\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eResult? This only matches when your \u003ccode\u003eTargetFramework\u003c/code\u003e is \u003ccode\u003enet8.0\u003c/code\u003e or \u003cstrong\u003elower\u003c/strong\u003e. Exactly the opposite of what you want.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe correct version:\u003c/strong\u003e\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=\"c\"\u003e\u0026lt;!-- CORRECT: Check if your target supports net8.0 features --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;net8.0\u0026#39;))\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;LangVersion\u0026gt;\u003c/span\u003e12.0\u003cspan class=\"nt\"\u003e\u0026lt;/LangVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eI wasted two hours debugging this exact issue last year. Felt like an idiot. Don\u0026rsquo;t be me.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"mistake-2-mixing-string-comparisons-with-property-functions\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#mistake-2-mixing-string-comparisons-with-property-functions\" title=\"Mistake #2: Mixing String Comparisons with Property Functions\"\u003eMistake #2: Mixing String Comparisons with Property Functions\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePick a strategy and stick with it:\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=\"c\"\u003e\u0026lt;!-- DON\u0026#39;T DO THIS: Mixing approaches --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(TargetFramework)\u0026#39; == \u0026#39;net8.0\u0026#39; OR $([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;net9.0\u0026#39;))\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;LangVersion\u0026gt;\u003c/span\u003e12.0\u003cspan class=\"nt\"\u003e\u0026lt;/LangVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis \u0026ldquo;works\u0026rdquo; but it\u0026rsquo;s confusing as hell. Why is \u003ccode\u003enet8.0\u003c/code\u003e handled with string comparison but \u003ccode\u003enet9.0\u003c/code\u003e uses the function? Future you (and everyone else on your team) will hate past you for this inconsistency.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBetter approach—be consistent:\u003c/strong\u003e\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=\"c\"\u003e\u0026lt;!-- Much clearer --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;PropertyGroup\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;net8.0\u0026#39;))\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;LangVersion\u0026gt;\u003c/span\u003e12.0\u003cspan class=\"nt\"\u003e\u0026lt;/LangVersion\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"mistake-3-forgetting-about-net-standard-compatibility\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#mistake-3-forgetting-about-net-standard-compatibility\" title=\"Mistake #3: Forgetting About .NET Standard Compatibility\"\u003eMistake #3: Forgetting About .NET Standard Compatibility\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET Standard is weird. A library targeting \u003ccode\u003enetstandard2.0\u003c/code\u003e works with:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e.NET Framework 4.6.1+\u003c/li\u003e\n\u003cli\u003e.NET Core 2.0+\u003c/li\u003e\n\u003cli\u003e.NET 5+\u003c/li\u003e\n\u003cli\u003eXamarin\u003c/li\u003e\n\u003cli\u003eUnity\u003c/li\u003e\n\u003cli\u003eBasically everything\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis creates tricky scenarios. Consider this common pattern:\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\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;$([MSBuild]::IsTargetFrameworkCompatible(\u0026#39;$(TargetFramework)\u0026#39;, \u0026#39;netstandard2.0\u0026#39;))\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=\"c\"\u003e\u0026lt;!-- Polyfills needed for .NET Standard but built-in for modern .NET --\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;System.Memory\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;4.5.5\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\u003eIf you forget that .NET Standard has its own compatibility rules, you\u0026rsquo;ll end up with missing dependencies on legacy platforms or unnecessary packages on modern ones.\u003c/p\u003e\n\u003cp\u003eThe function handles this correctly. String comparisons? Good luck expressing \u0026ldquo;compatible with .NET Standard 2.0 but not on platforms where it\u0026rsquo;s built-in\u0026rdquo; with string matching.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"but-what-about-performance\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#but-what-about-performance\" title=\"\u0026ldquo;But What About Performance?\u0026rdquo;\"\u003e\u0026ldquo;But What About Performance?\u0026rdquo;\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEvery time I recommend property functions over string comparisons, someone asks about performance overhead.\u003c/p\u003e\n\u003cp\u003eFair question. Let\u0026rsquo;s address it: \u003cstrong\u003ethe performance difference is completely irrelevant.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eMSBuild evaluates these conditions \u003cstrong\u003eonce per target framework\u003c/strong\u003e during project evaluation. Not per file. Not per build. Not continuously. Once.\u003c/p\u003e\n\u003cp\u003eWhether that evaluation takes 0.001ms (string comparison) or 0.002ms (property function) doesn\u0026rsquo;t matter when your total build time is measured in seconds or minutes.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s what actually costs you: \u003cstrong\u003eincorrect builds\u003c/strong\u003e. A build that fails intermittently because string conditions don\u0026rsquo;t match platform variants. A build that silently drops features because exact matching broke. A developer spending three hours debugging why CI fails when local builds work.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s the real cost. Not microseconds of MSBuild function calls.\u003c/p\u003e\n\u003cp\u003eString comparisons feel faster because they\u0026rsquo;re simpler. They\u0026rsquo;re not. They\u0026rsquo;re just fragile. And fragility in build configuration costs \u003cstrong\u003eway\u003c/strong\u003e more than execution time ever could.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migrating-existing-projects-without-breaking-everything\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#migrating-existing-projects-without-breaking-everything\" title=\"Migrating Existing Projects (Without Breaking Everything)\"\u003eMigrating Existing Projects (Without Breaking Everything)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSo you\u0026rsquo;ve got an existing project full of string-based TargetFramework conditions. How do you fix it without creating a regression nightmare?\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the approach that worked for me:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-1-find-all-the-string-comparisons\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#step-1-find-all-the-string-comparisons\" title=\"Step 1: Find All the String Comparisons\"\u003eStep 1: Find All the String Comparisons\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePowerShell makes this easy:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# Find all TargetFramework conditions in .csproj files\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eGet-ChildItem\u003c/span\u003e \u003cspan class=\"n\"\u003e-Recurse\u003c/span\u003e \u003cspan class=\"n\"\u003e-Filter\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;*.csproj\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003eSelect-String\u003c/span\u003e \u003cspan class=\"n\"\u003e-Pattern\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Condition.*TargetFramework\u0026#34;\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nb\"\u003eSelect-Object\u003c/span\u003e \u003cspan class=\"n\"\u003eFilename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eLineNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eLine\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis shows you exactly where the problems are. Don\u0026rsquo;t try to fix everything at once. Pick the highest-risk areas first—multi-targeting projects, platform-specific builds, anything in CI/CD pipelines.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-2-replace-strategically\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#step-2-replace-strategically\" title=\"Step 2: Replace Strategically\"\u003eStep 2: Replace Strategically\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eStart with the conditions that cause actual problems. If a string comparison works fine and never breaks, leave it for later. Focus on:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eMulti-targeting scenarios (where platform variants matter)\u003c/li\u003e\n\u003cli\u003eVersion-dependent package references (where \u0026ldquo;or higher\u0026rdquo; logic matters)\u003c/li\u003e\n\u003cli\u003ePlatform-specific feature flags (where semantic understanding matters)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eReplace the brittle ones first. Get the value immediately.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-3-test-multi-targeting-scenarios\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#step-3-test-multi-targeting-scenarios\" title=\"Step 3: Test Multi-Targeting Scenarios\"\u003eStep 3: Test Multi-Targeting Scenarios\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDon\u0026rsquo;t just test that it builds. Test that it builds \u003cstrong\u003eall targets correctly\u003c/strong\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Build each target framework explicitly\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet build -f net6.0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet build -f net8.0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet build -f net8.0-windows\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eVerify that conditions trigger when expected and don\u0026rsquo;t trigger when they shouldn\u0026rsquo;t. This catches parameter-reversal mistakes and logic errors.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-4-document-the-change\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#step-4-document-the-change\" title=\"Step 4: Document the Change\"\u003eStep 4: Document the Change\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUpdate your team\u0026rsquo;s build configuration standards. Add a section on TargetFramework conditions. Include examples. Make it clear that string comparisons are deprecated.\u003c/p\u003e\n\u003cp\u003eFuture developers (including future you) need to know the pattern. Otherwise, they\u0026rsquo;ll cargo-cult old string comparisons into new code, and you\u0026rsquo;re back to square one.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-5-add-a-code-review-checkpoint\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#step-5-add-a-code-review-checkpoint\" title=\"Step 5: Add a Code Review Checkpoint\"\u003eStep 5: Add a Code Review Checkpoint\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMake TargetFramework conditions part of your code review checklist. When someone adds or modifies framework-specific logic, verify they\u0026rsquo;re using property functions, not string comparisons.\u003c/p\u003e\n\u003cp\u003eThis prevents regression. You\u0026rsquo;ve cleaned up the mess. Don\u0026rsquo;t let it come back.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts-build-configuration-is-code\"\u003e\u003ca href=\"/posts/proper-use-of-targetframework-conditions/#final-thoughts-build-configuration-is-code\" title=\"Final Thoughts: Build Configuration Is Code\"\u003eFinal Thoughts: Build Configuration Is Code\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYour MSBuild conditions are part of your codebase. Treat them like production code, not like config files you can ignore.\u003c/p\u003e\n\u003cp\u003eString-based TargetFramework conditions might work today. They\u0026rsquo;ll fail tomorrow when requirements change. When you add a platform variant. When you upgrade to a newer framework version. When CI configuration drifts from local builds.\u003c/p\u003e\n\u003cp\u003eThese failures are \u003cstrong\u003esilent\u003c/strong\u003e. No exceptions. No error messages. Just features that mysteriously stop working. Builds that pass locally but fail in CI. Configurations that work by accident until they don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eMicrosoft built \u003ccode\u003eIsTargetFrameworkCompatible()\u003c/code\u003e to solve this exact problem. It\u0026rsquo;s been available for years. It handles all the edge cases. It understands framework semantics. It prevents the silent failures that string comparisons create.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUse it.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve debugged too many multi-targeting nightmares caused by string comparisons. I\u0026rsquo;ve watched senior developers lose days to build issues that should never have existed. I\u0026rsquo;ve seen production deployments break because someone added a platform specifier and half the project\u0026rsquo;s conditions stopped matching.\u003c/p\u003e\n\u003cp\u003eAll of it preventable. All of it caused by treating TargetFramework like a simple string instead of what it actually is—a semantic identifier that needs proper interpretation.\u003c/p\u003e\n\u003cp\u003eYour build configuration reflects your engineering discipline. Fragile string comparisons signal \u0026ldquo;good enough for now\u0026rdquo; thinking. Proper property functions signal \u0026ldquo;built to last\u0026rdquo; discipline.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eChoose wisely.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eWhen you add that next target framework—and you will—your build should just work. No silent failures. No missing features. No debugging sessions that start with \u0026ldquo;but it works on my machine.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s the difference between code that survives and code that scales. Between builds you trust and builds you fear. Between engineering and duct tape.\u003c/p\u003e\n\u003cp\u003eMicrosoft gave you the tools. Now use them correctly.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-06T17:30:00+01:00","id":"https://daily-devops.net/posts/proper-use-of-targetframework-conditions/","language":"en","summary":"String comparisons in TargetFramework conditions break multi-targeting builds. Here is why IsTargetFrameworkCompatible() exists and saves you hours.","tags":["msbuild","bestpractices","csharp","dotnet","softwareengineering"],"title":"Stop Breaking Multi-Targeting Builds with String Comparisons","url":"https://daily-devops.net/posts/proper-use-of-targetframework-conditions/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eAs .NET evolves, developers face an ever-growing tension between modern language features and the need to maintain compatibility across multiple frameworks. Applications no longer run in isolated environments; they live within ecosystems that combine .NET Framework, .NET Core, and .NET 6 or later. In such an environment, reliability and maintainability become the cornerstones of sustainable development. Defensive programming — the art of protecting your software against invalid inputs and unintended states — plays a crucial role in achieving this stability.\u003c/p\u003e\n\u003ca href=\"https://github.com/dailydevops/arguments\" class=\"linked\" target=\"_blank\" rel=\"noopener external noreferrer\" title=\"Provides a set of backward compatible `throw` helper methods, which have been added in previous .NET versions.\"\u003e\n  \u003cimg src=\"/images/github-dailydevops-arguments.png\" class=\"repository\" width=\"1200\" height=\"630\" title=\"Provides a set of backward compatible `throw` helper methods, which have been added in previous .NET versions.\" alt=\"Provides a set of backward compatible `throw` helper methods, which have been added in previous .NET versions.\" /\u003e\n\u003c/a\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/dailydevops/arguments\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cstrong\u003eNetEvolve.Arguments\u003c/strong\u003e\u003c/a\u003e library, published by DailyDevOps, takes this concept one step further. It provides a unified set of argument-validation helpers that mimic modern .NET throw-helper methods while remaining compatible with older target frameworks. In this article we explore how these defensive structures improve code quality, how they integrate with modern throw-helper APIs, and why compatibility across frameworks matters more than ever.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"defensive-programming-in-a-multi-framework-world\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#defensive-programming-in-a-multi-framework-world\" title=\"Defensive Programming in a Multi-Framework World\"\u003eDefensive Programming in a Multi-Framework World\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEvery experienced developer knows that the majority of runtime failures do not originate from flawed business logic but from invalid data. Null references, empty strings, invalid numeric ranges, or incomplete collections are classic sources of bugs that can easily be avoided with proper input validation. Defensive programming is the mindset that encourages developers to handle such conditions upfront. When applied consistently, it improves reliability and keeps business logic focused.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-multi-target-compatibility-problem\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#the-multi-target-compatibility-problem\" title=\"The Multi-Target Compatibility Problem\"\u003eThe Multi-Target Compatibility Problem\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHowever, modern .NET development rarely targets a single runtime. Many enterprise projects must simultaneously support .NET Standard 2.0, .NET 6, and .NET 8, often within the same solution. This multi-target approach quickly exposes inconsistencies, since not all framework versions include the same APIs for argument validation. What works elegantly in .NET 8 may not even compile in .NET Standard 2.0. Maintaining compatibility manually soon becomes tedious and error-prone.\u003c/p\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/dailydevops/arguments\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cstrong\u003eNetEvolve.Arguments\u003c/strong\u003e\u003c/a\u003e library was created precisely for this scenario. It bridges the gap between modern and legacy frameworks by providing a unified set of defensive programming tools that behave consistently, regardless of which runtime executes them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-evolution-of-native-throw-helpers-in-net\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#the-evolution-of-native-throw-helpers-in-net\" title=\"The Evolution of Native Throw-Helpers in .NET\"\u003eThe Evolution of Native Throw-Helpers in .NET\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft has gradually transformed how developers write argument validation. Before .NET 6, validation typically looked like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003earg\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ethrow\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentNullException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enameof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003earg\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWith .NET 6 came a fundamental improvement — the introduction of native throw-helper methods such as \u003ccode\u003eArgumentNullException.ThrowIfNull\u003c/code\u003e. This small but powerful addition removed boilerplate code and enhanced both readability and performance. Because the compiler can infer the argument name using the \u003ccode\u003e[CallerArgumentExpression]\u003c/code\u003e attribute, the developer no longer needs to repeat it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-net-7-and-8-added\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#what-net-7-and-8-added\" title=\"What .NET 7 And 8 Added\"\u003eWhat .NET 7 And 8 Added\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn .NET 7, this pattern was extended with \u003ccode\u003eArgumentException.ThrowIfNullOrEmpty\u003c/code\u003e, allowing developers to express string validation just as concisely. And with .NET 8, further methods like \u003ccode\u003eThrowIfZero\u003c/code\u003e, \u003ccode\u003eThrowIfNegative\u003c/code\u003e, and \u003ccode\u003eThrowIfGreaterThan\u003c/code\u003e have been added, enabling generic range validation across numeric types. These incremental improvements form a consistent language for defensive programming within .NET.\u003c/p\u003e\n\u003cp\u003eStatic code analysis has also adapted to this evolution. Rules such as \u003cstrong\u003eCA1510\u003c/strong\u003e and \u003cstrong\u003eCA1511\u003c/strong\u003e now explicitly encourage developers to prefer these throw-helper methods instead of traditional \u003ccode\u003eif\u003c/code\u003e blocks, citing benefits in performance and maintainability. For teams targeting the latest frameworks, the transition is natural and productive.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-legacy-frameworks-break-the-pattern\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#why-legacy-frameworks-break-the-pattern\" title=\"Why Legacy Frameworks Break The Pattern\"\u003eWhy Legacy Frameworks Break The Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe challenge, however, arises for developers maintaining multi-targeted libraries or legacy systems. Older frameworks simply lack these APIs. For example, .NET Standard 2.0 and .NET Framework 4.8 have no knowledge of \u003ccode\u003eArgumentException.ThrowIfNullOrEmpty\u003c/code\u003e. Without a compatibility layer, developers must either duplicate validation code or create conditional compilation blocks — both of which erode maintainability.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-netevolvearguments-exists\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#why-netevolvearguments-exists\" title=\"Why NetEvolve.Arguments Exists\"\u003eWhy NetEvolve.Arguments Exists\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/dailydevops/arguments\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cstrong\u003eNetEvolve.Arguments\u003c/strong\u003e\u003c/a\u003e library was designed to eliminate this fragmentation. It introduces a single, modern API that mirrors the behavior of the latest .NET throw-helpers while remaining compatible with all supported target frameworks. Developers can write expressive, modern code even when targeting legacy systems.\u003c/p\u003e\n\u003cp\u003eFor instance, consider the following example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrder\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003equantity\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eArgument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfLessThanOrEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003equantity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Business logic continues safely\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis style of validation is identical across frameworks. In .NET 8, it may delegate to the native throw-helper methods. In .NET Standard 2.0, it falls back to equivalent implementations provided by the library itself. The result is a clean and uniform developer experience that requires no conditional logic or framework-specific handling.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"practical-benefits-beyond-aesthetics\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#practical-benefits-beyond-aesthetics\" title=\"Practical Benefits Beyond Aesthetics\"\u003ePractical Benefits Beyond Aesthetics\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBeyond aesthetics, the approach yields practical benefits. Centralized throw-helpers ensure consistent exception messages and types. They make testing easier, as your unit tests can rely on uniform behavior regardless of the runtime. They also simplify code reviews, since validation logic follows a predictable pattern.\u003c/p\u003e\n\u003cp\u003eThe library’s core motivation is to combine modern expressiveness with backward compatibility — empowering teams to write future-ready code without abandoning their current runtime constraints.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"defensive-structures-in-practice\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#defensive-structures-in-practice\" title=\"Defensive Structures in Practice\"\u003eDefensive Structures in Practice\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAdopting a defensive mindset in .NET means validating everything that crosses a public boundary. Parameters, configuration values, external inputs, or even dependency injection results should be checked immediately. By enforcing these checks at the start of each method, you isolate invalid states early and ensure that downstream code operates under predictable conditions.\u003c/p\u003e\n\u003cp\u003eThe NetEvolve.Arguments library makes this both elegant and consistent. Whether you validate strings, numbers, or collections, the syntax remains uniform:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eArgument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eArgument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfLessThan\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eArgument\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrowIfNull\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"two-benefits-of-a-uniform-pattern\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#two-benefits-of-a-uniform-pattern\" title=\"Two Benefits Of A Uniform Pattern\"\u003eTwo Benefits Of A Uniform Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eOnce you establish this pattern throughout your project, you gain two important benefits. First, readability improves dramatically. Validation happens in one place at the top of the method, and the business logic that follows remains uncluttered. Second, your code base becomes self-documenting. Each guard clause communicates the preconditions of the method clearly and explicitly, turning runtime assumptions into executable contracts.\u003c/p\u003e\n\u003cp\u003eUnit testing complements this structure perfectly. By verifying that invalid inputs raise the appropriate exceptions, you build confidence in your defensive layer and ensure consistent behavior across frameworks. Because the library abstracts away framework differences, your tests remain valid for all targets.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"compatibility-as-a-design-principle\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#compatibility-as-a-design-principle\" title=\"Compatibility as a Design Principle\"\u003eCompatibility as a Design Principle\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompatibility is not just an implementation concern; it is a design principle. A well-architected .NET library must behave predictably no matter which runtime it runs on. The .NET team maintains strict guidelines for behavioral and binary compatibility across versions, and third-party libraries are expected to follow the same philosophy.\u003c/p\u003e\n\u003cp\u003eBy integrating NetEvolve.Arguments, developers inherit a consistent argument-validation API that adheres to this principle. There is no need for preprocessor directives or version-specific builds. The same guard clause pattern compiles and runs under .NET Framework, .NET Standard, and .NET 8 alike.\u003c/p\u003e\n\u003cp\u003eThis compatibility extends to deployment and maintenance as well. CI pipelines become simpler, because the same tests validate all target frameworks. Teams can refactor validation logic once and be confident that the change applies everywhere. The investment in defensive programming therefore yields both immediate and long-term stability.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"benefits-and-practical-impact\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#benefits-and-practical-impact\" title=\"Benefits and Practical Impact\"\u003eBenefits and Practical Impact\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe advantages of adopting a compatibility-aware defensive framework are multifaceted. It improves readability and reduces boilerplate code. It prevents subtle defects caused by missing argument checks. It fosters consistency across teams and projects. And most importantly, it creates a safety net that ensures software behaves as expected under all conditions.\u003c/p\u003e\n\u003cp\u003eThe trade-off is minimal. Each additional validation introduces a negligible runtime cost, but the resulting reliability far outweighs it. For performance-critical paths, developers can selectively disable guards while retaining them in higher layers. The flexibility remains entirely under your control.\u003c/p\u003e\n\u003cp\u003eBy leveraging the same API surface as the native .NET throw-helpers, you also future-proof your projects. When upgrading to newer runtimes, you do not need to rewrite your validation logic. The methods remain identical, ensuring a smooth transition.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/modern-defensive-programming/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModern .NET development emphasizes clarity, safety, and maintainability. The introduction of native throw-helper methods such as \u003ccode\u003eArgumentNullException.ThrowIfNull\u003c/code\u003e and \u003ccode\u003eArgumentException.ThrowIfNullOrEmpty\u003c/code\u003e represents a milestone in how developers express defensive intent. Yet many teams still need to support older frameworks, where these APIs are unavailable.\u003c/p\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/dailydevops/arguments\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cstrong\u003eNetEvolve.Arguments\u003c/strong\u003e\u003c/a\u003e library resolves this tension by providing a unified, backward-compatible API that works across all target frameworks. It captures the simplicity of modern .NET patterns while ensuring stability for legacy environments. The result is a clean, expressive, and sustainable approach to defensive programming — one that aligns with current best practices and remains compatible with the past.\u003c/p\u003e\n\u003cp\u003eIn a world of ever-changing frameworks and rapid release cycles, consistency is not a luxury but a necessity. With unified throw-helpers and thoughtful defensive structures, .NET developers can finally write once, validate everywhere, and trust their code to behave reliably — no matter which runtime it runs on.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-03T18:00:00+02:00","id":"https://daily-devops.net/posts/modern-defensive-programming/","language":"en","summary":"ArgumentNullException.ThrowIfNull modernizes .NET guard clauses; NetEvolve.Arguments gives a unified API across multi-framework target projects.\n","tags":["netevolve","softwareengineering","dotnet","csharp","nuget"],"title":"Modern Defensive Programming in .NET 8/9 with Throw Helpers","url":"https://daily-devops.net/posts/modern-defensive-programming/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eString formatting is everywhere in .NET applications: logging, debugging, user messages, dynamic content. Methods like \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.string.format\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003estring.Format\u003c/code\u003e\u003c/a\u003e and interpolated strings are convenient, but they have a cost: \u003cstrong\u003eparsing overhead\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eEvery time you call \u003ccode\u003estring.Format()\u003c/code\u003e, the runtime parses that format string to understand its structure, find placeholders, and figure out how to substitute values. When you use the same format string repeatedly (loops, logging, request handling), this parsing is pure waste. You\u0026rsquo;re doing the same work over and over.\u003c/p\u003e\n\u003cp\u003eEnter \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.text.compositeformat\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e\u003c/a\u003e, introduced in .NET 8. Parse a format string (\u003cstrong\u003esee\u003c/strong\u003e: \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/standard/base-types/composite-formatting\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eComposite formatting\u003c/a\u003e) \u003cstrong\u003eonce\u003c/strong\u003e, reuse it many times. No more repeated parsing, better performance. Simple concept, real impact.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-repeated-parsing-overhead\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#the-problem-repeated-parsing-overhead\" title=\"The Problem: Repeated Parsing Overhead\"\u003eThe Problem: Repeated Parsing Overhead\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a typical logging scenario:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing item {0} of {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Log or use the message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn this example, the format string \u003ccode\u003e\u0026quot;Processing item {0} of {1}\u0026quot;\u003c/code\u003e gets parsed 10,000 times. Same string, 10,000 parses. Each parse scans for placeholders, extracts format specifiers, validates structure, builds an internal representation. In a high-throughput app (web server, batch processor, real-time system), this adds up fast.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-solution-compositeformat\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#the-solution-compositeformat\" title=\"The Solution: CompositeFormat\"\u003eThe Solution: CompositeFormat\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e separates parsing from formatting:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Parse the format string once\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eformat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing item {0} of {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Reuse the parsed format many times\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eformat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e10000\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Log or use the message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eParse once, reuse the \u003ccode\u003eCompositeFormat\u003c/code\u003e instance. You just cut out 9,999 redundant operations.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"how-compositeformat-works\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#how-compositeformat-works\" title=\"How CompositeFormat Works\"\u003eHow CompositeFormat Works\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe internals are straightforward. \u003ccode\u003eCompositeFormat\u003c/code\u003e uses the same parsing logic as \u003ccode\u003estring.Format()\u003c/code\u003e, but stores the result for reuse.\u003c/p\u003e\n\u003cp\u003eWhen you call \u003ccode\u003eCompositeFormat.Parse(string)\u003c/code\u003e, the runtime scans the format string, validates it, and builds an internal representation (literal text + placeholders). That\u0026rsquo;s it, done once. When you call \u003ccode\u003estring.Format(IFormatProvider, CompositeFormat, ...)\u003c/code\u003e, the runtime skips parsing entirely and just substitutes values.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eCompositeFormat\u003c/code\u003e instance is immutable and thread-safe, so you can reuse it anywhere, even across threads. Classic .NET philosophy: if you\u0026rsquo;re doing the same thing repeatedly, don\u0026rsquo;t pay the cost every time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-benchmarks-real-world-impact\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#performance-benchmarks-real-world-impact\" title=\"Performance Benchmarks: Real-World Impact\"\u003ePerformance Benchmarks: Real-World Impact\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBenchmarks from the .NET runtime team show 15-30% reduction in execution time for repeated formatting operations, fewer allocations, less GC pressure, higher throughput in logging-heavy workloads.\u003c/p\u003e\n\u003cp\u003eThese gains matter in high-frequency scenarios: logging frameworks processing thousands of messages per second, request handlers, batch processing, telemetry systems.\u003c/p\u003e\n\u003cp\u003eTake a web API handling 50,000 requests per minute. Reduce formatting overhead by 20%, and you might handle 10,000 more requests on the same hardware. Lower CPU usage, lower latency. That\u0026rsquo;s real money saved.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-to-use-compositeformat\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#when-to-use-compositeformat\" title=\"When to Use CompositeFormat\"\u003eWhen to Use CompositeFormat\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUse \u003ccode\u003eCompositeFormat\u003c/code\u003e when the same format string gets used repeatedly: loops, hot paths, frequently called methods. It makes sense when performance matters (CPU-bound operations, latency reduction, high-throughput systems) and when you control the format string at compile time.\u003c/p\u003e\n\u003cp\u003eHigh-frequency logging is perfect for this. Parse once, reuse across thousands of log calls. Request/response handling, batch processing, performance-critical libraries—all good candidates.\u003c/p\u003e\n\u003cp\u003eDon\u0026rsquo;t use it for one-off formatting. Creating the \u003ccode\u003eCompositeFormat\u003c/code\u003e instance costs more than you save. Skip it for dynamic format strings that change at runtime. And for simple interpolated strings, just use \u003ccode\u003e$\u0026quot;...\u0026quot;\u003c/code\u003e. Readability matters.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"usage-examples\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#usage-examples\" title=\"Usage Examples\"\u003eUsage Examples\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"basic-pattern\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#basic-pattern\" title=\"Basic Pattern\"\u003eBasic Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eParse your format string once with \u003ccode\u003eCompositeFormat.Parse()\u003c/code\u003e, store it as \u003ccode\u003estatic readonly\u003c/code\u003e, reuse it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Parse format string once\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eLogFormat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;User {0} logged in at {1:yyyy-MM-dd HH:mm:ss}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Reuse many times\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e1000\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e++)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eLogFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"integration-pattern\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#integration-pattern\" title=\"Integration Pattern\"\u003eIntegration Pattern\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor larger apps, put all your format templates in one place:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eMessageFormats\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eErrorFormat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Error: {0} occurred at {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e \u003cspan class=\"n\"\u003eSuccessFormat\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eCompositeFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eParse\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Success: Operation {0} completed with result {1}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage across your application\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eerrorMessage\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eMessageFormats\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eErrorFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eexception\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDateTime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUtcNow\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWorks well with dependency injection, keeps formatting consistent across your app.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"integration-with-existing-code\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#integration-with-existing-code\" title=\"Integration with Existing Code\"\u003eIntegration with Existing Code\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou don\u0026rsquo;t need to rewrite everything. Profile your code (dotTrace, \u003ca href=\"https://github.com/microsoft/perfview\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003ePerfView\u003c/a\u003e), find the hot format strings, extract them to \u003ccode\u003estatic readonly\u003c/code\u003e fields, swap the method calls. Benchmark before and after.\u003c/p\u003e\n\u003cp\u003eMigration is usually just extracting a string literal and changing a method call. Small change, real impact.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"best-practices\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#best-practices\" title=\"Best Practices\"\u003eBest Practices\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCache instances as \u003ccode\u003estatic readonly\u003c/code\u003e fields. Focus on hot paths: loops, high-frequency methods, performance-critical code. Benchmark with BenchmarkDotNet. Keep format strings simple. Thread-safe by default. Combine with \u003ccode\u003eStringBuilder\u003c/code\u003e when building complex strings.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bigger-picture\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#the-bigger-picture\" title=\"The Bigger Picture\"\u003eThe Bigger Picture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e fits into .NET\u0026rsquo;s broader push for zero-cost abstractions. \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e and \u003ccode\u003eMemory\u0026lt;T\u0026gt;\u003c/code\u003e for zero-allocation slicing. \u003ccode\u003eArrayPool\u0026lt;T\u0026gt;\u003c/code\u003e for object pooling. \u003ccode\u003eValueTask\u0026lt;T\u0026gt;\u003c/code\u003e for allocation-free async. Source generators for compile-time code generation. Native AOT for faster startup.\u003c/p\u003e\n\u003cp\u003eThe pattern is consistent: control over performance without sacrificing usability. Opt-in when you need it, invisible when you don\u0026rsquo;t.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"evolution-across-net-versions\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#evolution-across-net-versions\" title=\"Evolution Across .NET Versions\"\u003eEvolution Across .NET Versions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e landed in .NET 8. Each release since has made it better.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e.NET 9\u003c/strong\u003e optimized internals. Same API, faster formatting engine. Fewer allocations, especially with many placeholders. Less GC pressure.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e.NET 10\u003c/strong\u003e improved JIT compiler understanding. More aggressive inlining for repeated formatting. Better interop with \u003ccode\u003eSpan\u0026lt;char\u0026gt;\u003c/code\u003e and \u003ccode\u003eMemory\u0026lt;char\u0026gt;\u003c/code\u003e for allocation-free scenarios.\u003c/p\u003e\n\u003cp\u003eUpgrading from .NET 6 or 7 to .NET 8+ gets you \u003ccode\u003eCompositeFormat\u003c/code\u003e plus a faster runtime overall.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/compositeformat-performance-boost/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCompositeFormat\u003c/code\u003e is small but effective. Parse once, format many times. Less CPU, fewer allocations, better throughput.\u003c/p\u003e\n\u003cp\u003eThe gains are real: logging, request handling, batch processing all benefit. It\u0026rsquo;s opt-in, so adopt it incrementally without breaking existing code.\u003c/p\u003e\n\u003cp\u003eProfile your hot paths, find repeated formatting, switch to \u003ccode\u003eCompositeFormat\u003c/code\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSimple change, \u003cstrong\u003emeasurable results.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-23T17:00:00+02:00","id":"https://daily-devops.net/posts/compositeformat-performance-boost/","language":"en","summary":"Parse once, format a thousand times. CompositeFormat eliminates redundant parsing overhead and makes your .NET apps faster with one simple change.","tags":["performance","dotnet","csharp","bestpractices","hidden-gems","softwareengineering"],"title":"Stop Parsing the Same String Twice: CompositeFormat in .NET","url":"https://daily-devops.net/posts/compositeformat-performance-boost/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eEveryone preaches Clean Code. Few deliver it. Even fewer can explain the purpose behind it.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eClean Code has become a buzzword in software development. It promises clarity, maintainability, and professionalism. Yet, in many projects, especially within the .NET ecosystem, the pursuit of Clean Code has devolved into a superficial exercise — a checklist of patterns and practices that often obscures rather than reveals intent.\u003c/p\u003e\n\u003cp\u003eWhat began as a philosophy of craftsmanship has become a slogan. Across the software industry, entire companies promote themselves as \u0026ldquo;Clean Code\u0026rdquo; experts. They quote principles, host workshops, and promise maintainable systems built on solid engineering ethics. But when you take over one of their projects, the illusion often breaks quickly.\u003c/p\u003e\n\u003cp\u003eBehind the neat folder structures and the spotless naming conventions, you find the opposite of maintainability: deep abstraction hierarchies, duplicated logic, and decisions made to look professional rather than to last. The surface is clean, but the foundation is fragile. Clean Code, in these environments, has turned from a discipline into a decoration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-clean-turns-into-clutter\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#when-clean-turns-into-clutter\" title=\"When Clean Turns Into Clutter\"\u003eWhen Clean Turns Into Clutter\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe intent behind Clean Code is noble. Readability, simplicity, and maintainability have always been pillars of good software. Yet, in many .NET projects, the application of these ideas drifts into over-engineering.\u003c/p\u003e\n\u003cp\u003eDevelopers eager to demonstrate \u0026ldquo;good design\u0026rdquo; create layers of repositories, services, and managers that add distance rather than clarity. Patterns are applied mechanically instead of meaningfully. C# makes such designs easy to express, but without discipline, they create noise instead of structure.\u003c/p\u003e\n\u003cp\u003eIn effective systems, every layer exists for a reason. It isolates complexity or stabilizes a contract. In misguided ones, layers multiply because someone once said \u0026ldquo;that\u0026rsquo;s how clean code should look.\u0026rdquo; The result is the opposite of clarity: a maze of abstractions where simplicity should have lived.\u003c/p\u003e\n\u003cp\u003eClean Code was never about purity. It was about communication, code that speaks its purpose clearly and succinctly.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"how-to-recognize-a-clean-code-disaster\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#how-to-recognize-a-clean-code-disaster\" title=\"How to Recognize a Clean Code Disaster\"\u003eHow to Recognize a Clean Code Disaster\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou don\u0026rsquo;t have to read the code to know when a Clean Code project has failed. Product Owners, Scrum Masters, and technical managers can identify the warning signs long before the architecture diagram gives it away.\u003c/p\u003e\n\u003cp\u003eWhen development velocity drops without visible cause, you are likely seeing the impact of unnecessary complexity. Teams spend more time understanding the structure than implementing logic. Planning sessions get longer, and \u0026ldquo;small\u0026rdquo; changes suddenly take entire sprints.\u003c/p\u003e\n\u003cp\u003eWhen developers start discussing patterns, interfaces, or naming more than business outcomes, philosophy has overtaken purpose. That shift from solving problems to defending design purity is the hallmark of a Clean Code disaster.\u003c/p\u003e\n\u003cp\u003eIf onboarding new team members feels like teaching theology instead of engineering, you are no longer running a project — you are managing a doctrine.\u003c/p\u003e\n\u003cp\u003eThese projects are easy to recognize: they look perfect in review slides, but nobody can confidently add a new feature. Clean Code has become an excuse for paralysis.\u003c/p\u003e\n\u003cp\u003eWondering how to handle all kinds of technical debt? You might find inspiration in my articles \u003cstrong\u003e\u003ca href=\"https://daily-devops.net/posts/illuminate-technical-debt/\"\u003eIlluminate Technical Debt\u003c/a\u003e\u003c/strong\u003e or \u003cstrong\u003e\u003ca href=\"https://daily-devops.net/posts/tale-of-forgotten-pennies-and-lost-dollars/\"\u003eA Tale of Forgotten Pennies and Lost Dollars\u003c/a\u003e\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-subjectivity-trap\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#the-subjectivity-trap\" title=\"The Subjectivity Trap\"\u003eThe Subjectivity Trap\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eClean Code\u0026rsquo;s biggest flaw is its subjectivity. What one developer considers elegant, another sees as excessive. Without shared standards, teams drift toward inconsistency. Over time, that inconsistency turns into entropy.\u003c/p\u003e\n\u003cp\u003eThis is where the .NET ecosystem provides real strength — if teams use it.\u003c/p\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s official \u003cstrong\u003e\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eC# Coding Conventions\u003c/a\u003e\u003c/strong\u003e offer consistent guidance that prevents personal interpretation from dominating code style. They cover essential ground: meaningful naming, predictable indentation, placement of braces, and clear method intent. They sound simple, but simplicity is precisely the point — clarity begins with habit.\u003c/p\u003e\n\u003cp\u003eBeyond syntax, the \u003cstrong\u003e\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eFramework Design Guidelines\u003c/a\u003e\u003c/strong\u003e by Krzysztof Cwalina and Brad Abrams extend these ideas into design maturity. They encourage minimal public exposure, predictable method naming, immutable data where feasible, and the separation of domain and infrastructure concerns. These aren\u0026rsquo;t arbitrary conventions; they\u0026rsquo;re principles proven through the evolution of .NET itself.\u003c/p\u003e\n\u003cp\u003eComplementary to that, tools such as \u003cstrong\u003e.editorconfig\u003c/strong\u003e and \u003cstrong\u003eRoslyn Analyzers\u003c/strong\u003e allow you to codify these rules directly into your build pipeline. They turn subjective ideals into enforceable practice — removing \u0026ldquo;it looks cleaner\u0026rdquo; from every review conversation.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eReal Clean Code doesn\u0026rsquo;t rely on taste. It relies on consistency.\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"the-clean-code-business-model\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#the-clean-code-business-model\" title=\"The Clean Code Business Model\"\u003eThe Clean Code Business Model\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMany consulting firms have learned to commercialize the language of Clean Code. They brand it as proof of engineering excellence and build delivery models around it. Unfortunately, much of this is theater.\u003c/p\u003e\n\u003cp\u003eThese firms often deliver code that passes inspection — it compiles neatly, adheres to style rules, and satisfies every static analysis tool — yet still lacks coherence. When you extend it, you discover how rigid it really is. Each minor change requires revisiting abstractions that were meant to protect flexibility. The system becomes elegant but immobile.\u003c/p\u003e\n\u003cp\u003eThis happens because the focus shifts from \u003cem\u003eevolution\u003c/em\u003e to \u003cem\u003epresentation\u003c/em\u003e. The goal is to appear clean, not to stay changeable. The product is technically compliant but practically suffocating. Clean Code, stripped of pragmatism, turns into an architectural straitjacket.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-clean-code-becomes-a-liability\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#when-clean-code-becomes-a-liability\" title=\"When Clean Code Becomes a Liability\"\u003eWhen Clean Code Becomes a Liability\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSoftware engineering always operates under constraint. Budgets, deadlines, and shifting priorities dictate reality. Clean Code, when treated as a moral requirement instead of a practical discipline, often ignores those constraints.\u003c/p\u003e\n\u003cp\u003eEvery abstraction, every refactor, every additional layer has a cost. When those costs go unacknowledged, the project accumulates \u003cstrong\u003estructural debt\u003c/strong\u003e, code that is technically ideal but functionally rigid. It cannot evolve without risk.\u003c/p\u003e\n\u003cp\u003eThe irony is sharp: the same projects that advertise \u0026ldquo;Clean Code\u0026rdquo; often become the hardest to maintain. They have confused clarity with complexity, principles with efficiency.\u003c/p\u003e\n\u003cp\u003eWhen code is written for the slide deck instead of the sprint, it becomes a \u003cem\u003eliability\u003c/em\u003e, not an asset.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-integrity-and-sustainable-clarity\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#practical-integrity-and-sustainable-clarity\" title=\"Practical Integrity and Sustainable Clarity\"\u003ePractical Integrity and Sustainable Clarity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eReal Clean Code is grounded in restraint. It means writing C# that is understandable, testable, and predictable, without turning simplicity into ceremony.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"apply-patterns-with-purpose\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#apply-patterns-with-purpose\" title=\"Apply Patterns with Purpose\"\u003eApply Patterns with Purpose\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDependency injection, for example, should be used to support modularity and testing, not to decorate trivial classes. Asynchronous code should express intent clearly — methods named \u003cstrong\u003e\u003ccode\u003eGetAsync\u003c/code\u003e\u003c/strong\u003e should do exactly what they promise — and mixing synchronous and asynchronous patterns should be avoided. State should be explicit, and side effects should be visible.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"follow-framework-conventions\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#follow-framework-conventions\" title=\"Follow Framework Conventions\"\u003eFollow Framework Conventions\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGood C# code follows the spirit of the platform. It leverages the framework\u0026rsquo;s conventions rather than fighting them. The \u003cstrong\u003e\u003ca href=\"https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eDesign guidelines for developing class libraries\u003c/a\u003e\u003c/strong\u003e explicitly recommend favoring readability, minimizing surprise, and maintaining a predictable object model. Following them doesn\u0026rsquo;t just improve code; it builds trust between developers who may never meet but must share the same repository.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eReadable code is not the goal; it is the byproduct of deliberate design choices that make collaboration sustainable.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"conclusion-clean-code-as-practice-not-theater\"\u003e\u003ca href=\"/posts/clean-code-lip-service-not-a-standard/#conclusion-clean-code-as-practice-not-theater\" title=\"Conclusion: Clean Code as Practice, Not Theater\"\u003eConclusion: Clean Code as Practice, Not Theater\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eClean Code is not the destination. It is the baseline — a way of showing that you care about what comes next.\u003c/p\u003e\n\u003cp\u003eTrue engineering excellence begins where Clean Code ends: in architecture that aligns with context, in systems that evolve gracefully, and in decisions that respect both business goals and human comprehension.\u003c/p\u003e\n\u003cp\u003eCompanies that sell Clean Code as a brand often leave behind systems that cannot grow. They confuse purity with professionalism and structure with sustainability.\u003c/p\u003e\n\u003cp\u003eGood software is written for people as much as for machines.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eMy motto: \u003cstrong\u003eStick to the framework.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn .NET, that means trusting the conventions, libraries, and design wisdom refined over decades rather than chasing ideological perfection.\u003c/p\u003e\n\u003cp\u003eClean Code, when practiced honestly, is not theater. It is a quiet act of respect, respect for the craft, for the product, and for the next developer who must live with your decisions.\u003c/p\u003e\n","date_modified":"2026-02-13T11:27:21+01:00","date_published":"2025-10-16T13:00:00+02:00","id":"https://daily-devops.net/posts/clean-code-lip-service-not-a-standard/","language":"en","summary":"How misunderstood Clean Code ideals harm .NET systems. Learn to recognize code quality failures and apply C# best practices for maintainable software.","tags":["csharp","dotnet","technicaldebt","softwareengineering","bestpractices","codequality"],"title":"Clean Code: A Lip Service, Not a Standard\n","url":"https://daily-devops.net/posts/clean-code-lip-service-not-a-standard/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the pursuit of high-performance .NET applications, every optimization counts.\nWith .NET 7, Microsoft introduced the \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.constantexpectedattribute\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e\u003c/a\u003e, a seemingly simple addition that unlocks significant compiler-level optimizations and improves developer experience.\nThis attribute signals to the compiler and analyzers that a parameter is expected to be a constant value, enabling aggressive optimizations and better tooling support.\u003c/p\u003e\n\u003cp\u003eBut what makes this attribute truly valuable? Let\u0026rsquo;s explore its benefits and practical applications.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-is-constantexpectedattribute\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#what-is-constantexpectedattribute\" title=\"What is ConstantExpectedAttribute?\"\u003eWhat is ConstantExpectedAttribute?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e is defined in the \u003ccode\u003eSystem.Diagnostics.CodeAnalysis\u003c/code\u003e namespace and is applied to method parameters to indicate that the compiler should expect a constant value at the call site. When applied, it serves two primary purposes: it acts as a compiler optimization signal that informs the JIT compiler that it can safely perform constant folding and other optimizations, and it provides developer guidance by supplying IDE analyzers with information to warn when non-constant values are passed.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eConfigureLogging\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected]\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Implementation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis simple annotation enables the compiler to make intelligent decisions about code generation, potentially eliminating branches, inlining code, or pre-computing values at compile time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-performance-benefits\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#the-performance-benefits\" title=\"The Performance Benefits\"\u003eThe Performance Benefits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUnderstanding the theoretical benefits of \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e is one thing, but seeing its practical impact on code optimization reveals its true power. The attribute enables several sophisticated compiler optimizations that directly translate to faster execution times and more efficient resource utilization.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"constant-folding-and-dead-code-elimination\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#constant-folding-and-dead-code-elimination\" title=\"Constant Folding and Dead Code Elimination\"\u003eConstant Folding and Dead Code Elimination\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen the compiler knows a value is constant, it can perform constant folding by evaluating expressions at compile time rather than runtime. This is particularly powerful in hot paths where every CPU cycle matters.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLogger\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [ConstantExpected]\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eWriteDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eWriteInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eError\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eWriteError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Usage with constant\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInfo\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Processing started\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWithout the attribute, the compiler must generate code that evaluates all three conditional branches at runtime. With \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, when the compiler sees a constant value like \u003ccode\u003eLogLevel.Info\u003c/code\u003e, it eliminates dead branches by removing the Debug and Error checks entirely, inlines the \u003ccode\u003eWriteInfo\u003c/code\u003e method call directly, and generates smaller, more cache-friendly machine code.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"register-allocation-and-branch-prediction\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#register-allocation-and-branch-prediction\" title=\"Register Allocation and Branch Prediction\"\u003eRegister Allocation and Branch Prediction\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConstant values can be loaded directly into CPU registers rather than fetched from memory, reducing latency. Additionally, by eliminating branches through constant folding, the CPU\u0026rsquo;s branch predictor has fewer decisions to make, reducing pipeline stalls. Modern processors occasionally mispredict branches, resulting in pipeline flushes that waste dozens of cycles. When the compiler eliminates branches entirely, these prediction failures become impossible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"enhanced-ide-and-analyzer-support\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#enhanced-ide-and-analyzer-support\" title=\"Enhanced IDE and Analyzer Support\"\u003eEnhanced IDE and Analyzer Support\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBeyond runtime performance, the attribute improves the developer experience by making the compiler\u0026rsquo;s expectations explicit and enabling sophisticated static analysis.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"compile-time-warnings\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#compile-time-warnings\" title=\"Compile-Time Warnings\"\u003eCompile-Time Warnings\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eModern IDEs like Visual Studio and Rider can detect when non-constant values are passed to parameters marked with this attribute (see \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/ide/quick-actions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eQuick Actions\u003c/a\u003e):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// IDE Warning: Parameter expects a constant value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edynamicLevel\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGetLogLevelFromConfig\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edynamicLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;This will generate a warning\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis immediate feedback helps developers catch potential performance issues during development rather than in production, shifting performance optimization left in the development lifecycle where it\u0026rsquo;s cheaper to fix. Teams can configure build systems to treat these warnings as errors in performance-critical modules, creating automated guardrails that maintain code quality.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"api-contract-clarity\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#api-contract-clarity\" title=\"API Contract Clarity\"\u003eAPI Contract Clarity\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute serves as documentation in code, making it explicit that certain parameters are designed for constant values:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// Configures the retry policy with the specified number of attempts.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;/summary\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;param name=\u0026#34;maxAttempts\u0026#34;\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// The maximum number of retry attempts. \u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// This should be a compile-time constant for optimal performance.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cs\"\u003e/// \u0026lt;/param\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSetRetryPolicy\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected(Min = 1, Max = 10)]\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003emaxAttempts\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Implementation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen developers encounter this method, they immediately understand not just what the parameter does, but how it should be used for optimal performance. The \u003ccode\u003eMin\u003c/code\u003e and \u003ccode\u003eMax\u003c/code\u003e constraints further clarify the valid range, providing both documentation and compile-time validation in a single declaration. This reduces cognitive load by providing immediate, actionable guidance through IntelliSense.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"min-and-max-constraints\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#min-and-max-constraints\" title=\"Min and Max Constraints\"\u003eMin and Max Constraints\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe attribute supports optional \u003ccode\u003eMin\u003c/code\u003e and \u003ccode\u003eMax\u003c/code\u003e properties to specify expected value ranges:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eSetThreadPoolSize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected(Min = 1, Max = 64)]\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ethreadCount\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Compiler knows threadCount is between 1 and 64\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Can optimize bounds checking and array allocations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRange constraints enable additional optimizations such as bounds check elimination, loop unrolling, and stack allocation. When the compiler knows a value is within a specific range, it can eliminate defensive bounds checks and make intelligent decisions about memory allocation strategies. For example, if the thread count is guaranteed to be between 1 and 64, the compiler can allocate a fixed-size array on the stack rather than the heap.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"design-considerations\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#design-considerations\" title=\"Design Considerations\"\u003eDesign Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhile \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e offers significant benefits, thoughtful application ensures maximum value without introducing unnecessary constraints.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-use\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#when-to-use\" title=\"When to Use\"\u003eWhen to Use\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute is particularly valuable in hot paths where methods are called frequently. Consider configuration parameters typically known at compile time, feature flags that act as boolean switches, and mathematical constants such as fixed exponents. APIs called in tight loops or operations occurring millions of times per second benefit most, where the overhead of a conditional branch multiplied across millions of invocations becomes measurable.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-avoid\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#when-to-avoid\" title=\"When to Avoid\"\u003eWhen to Avoid\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute should be avoided for user input from runtime sources, dynamic configuration loaded from files or databases, and public API parameters where callers might pass variables. Applying the attribute to parameters that rarely receive constant values creates unnecessary warnings. The attribute should reflect actual usage patterns rather than idealized scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"backward-compatibility\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#backward-compatibility\" title=\"Backward Compatibility\"\u003eBackward Compatibility\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe attribute has no runtime effect and doesn\u0026rsquo;t change method signatures. Adding it to existing code is non-breaking, removing it doesn\u0026rsquo;t affect compiled consumers, and it\u0026rsquo;s purely a compile-time hint. This makes \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e an excellent candidate for incremental adoption without coordinating breaking changes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-world-impact\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#real-world-impact\" title=\"Real-World Impact\"\u003eReal-World Impact\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a high-throughput logging system processing millions of messages per second:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Before: Dynamic log level check\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_minimumLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eWriteToSink\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// After: With ConstantExpectedAttribute\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected]\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003e_minimumLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eWriteToSink\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn real-world benchmarks, using \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e with constant log levels resulted in a 15 to 20 percent reduction in CPU time. Measurements from a production API gateway processing over 10 million requests per hour showed measurably reduced CPU utilization, translating to cost savings and improved latency. The code size of hot logging paths decreased by approximately 30 percent, contributing to improved cache efficiency.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"integration-with-source-generators\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#integration-with-source-generators\" title=\"Integration with Source Generators\"\u003eIntegration with Source Generators\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSource generators pair well with \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, enabling compile-time code generation that leverages constant values:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[LoggerMessage(\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    EventId = 1, \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    Level = LogLevel.Information,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    Message = \u0026#34;Processing request {RequestId}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLogProcessing\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [ConstantExpected]\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eeventId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003erequestId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen source generators encounter methods with \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, they can generate specialized implementations optimized for constant-value scenarios. For example, a logging generator might emit code that directly maps event IDs to log messages without dictionary lookups, creating a two-stage optimization where the generator produces optimized code at compile time, and the JIT compiler further optimizes based on constant hints.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"evolution-through-net-versions\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#evolution-through-net-versions\" title=\"Evolution Through .NET Versions\"\u003eEvolution Through .NET Versions\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhile \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e was introduced in .NET 7, the compiler infrastructure around it has continuously improved, making the attribute increasingly valuable with each release.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-8-foundation-and-stability\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#net-8-foundation-and-stability\" title=\".NET 8: Foundation and Stability\"\u003e.NET 8: Foundation and Stability\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn .NET 8 (November 2023), \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e remained stable while the overall compiler optimization pipeline improved. Enhanced Profile-Guided Optimization (PGO) and JIT compilation techniques meant the compiler could better use constant hints. The .NET 8 runtime\u0026rsquo;s improved method inlining and loop optimization worked synergistically with \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e to deliver better performance where constant parameters were prevalent.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-9-enhanced-attribute-ecosystem\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#net-9-enhanced-attribute-ecosystem\" title=\".NET 9: Enhanced Attribute Ecosystem\"\u003e.NET 9: Enhanced Attribute Ecosystem\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 9 (November 2024) introduced complementary attributes like \u003ccode\u003eFeatureSwitchDefinitionAttribute\u003c/code\u003e and \u003ccode\u003eFeatureGuardAttribute\u003c/code\u003e that expanded the attribute-based optimization paradigm. These attributes work similarly by treating properties as constants during compilation, enabling dead code elimination. Runtime improvements, including enhanced \u003ccode\u003eUnsafeAccessorAttribute\u003c/code\u003e support and DATAS garbage collection optimizations, created an environment where constant-aware code performed even better.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-10-looking-forward\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#net-10-looking-forward\" title=\".NET 10: Looking Forward\"\u003e.NET 10: Looking Forward\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 10 (preview, LTS planned November 2025) brings substantial runtime and JIT improvements including de-virtualization of array interface methods, inlining of late de-virtualized methods, and stack allocation of small arrays. When constant parameters determine array sizes or iteration counts, the runtime makes more intelligent decisions about stack allocation and loop unrolling. The JIT compiler can now inline methods across more complex scenarios, creating optimization opportunities that were previously impossible.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"practical-implications\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#practical-implications\" title=\"Practical Implications\"\u003ePractical Implications\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe stability of \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e across .NET 7 through 10 demonstrates forward-thinking design. While the attribute\u0026rsquo;s API surface remains constant, its effectiveness grows with each release as the compiler and runtime become more sophisticated. Code written for .NET 7 with this attribute runs faster on .NET 8, even faster on .NET 9, and faster still on .NET 10, without source code changes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bigger-picture-compiler-developer-collaboration\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#the-bigger-picture-compiler-developer-collaboration\" title=\"The Bigger Picture: Compiler-Developer Collaboration\"\u003eThe Bigger Picture: Compiler-Developer Collaboration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e represents a broader trend in .NET development: closer collaboration between developer intent and compiler optimization. Similar attributes like \u003ccode\u003e[StringSyntax]\u003c/code\u003e, \u003ccode\u003e[RequiresUnreferencedCode]\u003c/code\u003e, and \u003ccode\u003e[DynamicallyAccessedMembers]\u003c/code\u003e bridge the gap between human understanding and machine optimization.\u003c/p\u003e\n\u003cp\u003eThis approach enables progressive enhancement where code works without the attribute but performs better with it, has zero runtime cost since it exists only at compile time, and makes intent explicit through self-documenting API contracts. Modern .NET allows developers to express intent through attributes while the compiler handles complex optimization work, democratizing performance optimization for developers without deep compiler knowledge.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-adoption-strategy\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#practical-adoption-strategy\" title=\"Practical Adoption Strategy\"\u003ePractical Adoption Strategy\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFor teams adopting \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e, start by profiling to identify hot paths where constant parameters are common. Begin with logging and configuration methods where benefits are most apparent. Measure impact using benchmarks like BenchmarkDotNet to validate improvements. Choose APIs where constant expectations align naturally with usage patterns—logging frameworks work well because log levels are almost always constants in production code.\u003c/p\u003e\n\u003cp\u003eRemember that the attribute should reflect actual usage patterns rather than idealized scenarios. Apply it where it adds value, not everywhere possible. The goal is meaningful performance improvements in critical code paths, not comprehensive attribute coverage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-takeaways\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#key-takeaways\" title=\"Key Takeaways\"\u003eKey Takeaways\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e demonstrates how modern .NET bridges the gap between developer intent and compiler optimization. On the performance front, it enables constant folding, dead code elimination, and register optimization, delivering measurable gains of 15-20% in hot paths where methods are called millions of times. From a developer experience perspective, the attribute provides compile-time warnings and self-documenting APIs that make performance expectations explicit, catching potential issues during development rather than production. Throughout its evolution, the attribute has remained stable across .NET versions while automatically benefiting from each release\u0026rsquo;s improved optimization infrastructure—code written for .NET 7 runs faster on .NET 10 without modifications. For practical adoption, teams should start small with logging and configuration methods, measure results using benchmarks, and expand based on proven value rather than comprehensive coverage.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts\"\u003e\u003ca href=\"/posts/constant-expected-attribute/#final-thoughts\" title=\"Final Thoughts\"\u003eFinal Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e exemplifies modern .NET\u0026rsquo;s philosophy: provide powerful, optional tools that progressively enhance code quality without breaking changes. It\u0026rsquo;s not syntactic sugar—it\u0026rsquo;s a performance optimization tool that makes developer intent machine-readable.\u003c/p\u003e\n\u003cp\u003eAs compilers become more sophisticated, we can focus more on solving business problems and less on manual optimization. \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e represents a future where performance and productivity complement rather than compete. By adopting it thoughtfully in performance-critical code, you invest in applications that not only run faster today but will continue to improve automatically as the .NET platform evolves.\u003c/p\u003e\n\u003cp\u003eIn the end, the best optimizations are those that align with natural code patterns. When constant values are the norm and performance matters, \u003ccode\u003eConstantExpectedAttribute\u003c/code\u003e transforms compiler awareness into measurable gains—effortlessly.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-14T14:30:00+02:00","id":"https://daily-devops.net/posts/constant-expected-attribute/","language":"en","summary":"How ConstantExpectedAttribute in .NET 7+ enables compile-time optimizations, better IDE support, and improved performance via constant signaling.","tags":["performance","bestpractices","csharp","dotnet","softwareengineering"],"title":"ConstantExpectedAttribute: Compile-Time Performance","url":"https://daily-devops.net/posts/constant-expected-attribute/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn every mature .NET landscape, legacy projects represent both heritage and hazard.\nThey once powered entire business models — now they silently consume time, budget, and attention.\nThe decision to retire or modernize them isn’t about technology fashion. It’s about sustaining the organization’s \u003cstrong\u003ecapacity for value creation\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-real-cost-of-staying-still\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#the-real-cost-of-staying-still\" title=\"The Real Cost of Staying Still\"\u003eThe Real Cost of Staying Still\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLegacy .NET systems rarely collapse overnight. Instead, they leak productivity, talent, and opportunity.\nAcross enterprise portfolios, the same pattern emerges: \u003cstrong\u003edeath by a thousand cuts\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eEach day brings smaller compromises. A deployment that takes three hours instead of ten minutes. A critical patch delayed because nobody remembers how the authentication module works. A talented developer who leaves because they\u0026rsquo;re tired of debugging 15-year-old code with zero documentation.\u003c/p\u003e\n\u003cp\u003eThe symptoms compound silently:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTechnical debt interest\u003c/strong\u003e accumulates faster than principal payments\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKnowledge gaps\u003c/strong\u003e widen as original developers retire or move on\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSecurity vulnerabilities\u003c/strong\u003e multiply in unmaintained dependencies\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePerformance degradation\u003c/strong\u003e becomes \u0026ldquo;just how the system works\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntegration bottlenecks\u003c/strong\u003e prevent adoption of modern tools and platforms\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eOrganizations often don\u0026rsquo;t realize the true cost until it\u0026rsquo;s measured. One mid-size company discovered their legacy .NET Framework applications consumed \u003cstrong\u003e40% of their total IT budget\u003c/strong\u003e while delivering only \u003cstrong\u003e12% of new business value\u003c/strong\u003e. The remainder went to keeping old systems breathing — patching, monitoring, and working around limitations that modern architectures solve by design.\u003c/p\u003e\n\u003cp\u003eMeanwhile, competitors move faster. They deploy daily instead of quarterly. They scale automatically instead of manually provisioning servers. They innovate while you maintain.\u003c/p\u003e\n\u003cp\u003eThe most insidious cost isn\u0026rsquo;t technical — it\u0026rsquo;s \u003cstrong\u003eopportunity cost\u003c/strong\u003e. Every hour spent nursing legacy systems is an hour not spent building competitive advantage.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eFactor\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eTypical Overhead\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eBusiness Impact\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eMaintenance \u0026amp; Support\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e+25–35 %\u003c/strong\u003e vs. modern systems\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eSlower response, fragile builds\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eSecurity \u0026amp; Compliance\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e2× audit effort\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eManual patching, unsupported dependencies\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eTalent Retention\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e+20–40 % cost\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eFewer developers skilled in legacy tech\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eDelivery Velocity\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e−30 % throughput\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003eMissed releases and delayed features\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eIn short: legacy code is not just \u003cem\u003eold\u003c/em\u003e. It’s \u003cem\u003eexpensive to keep right\u003c/em\u003e and \u003cem\u003erisky to ignore\u003c/em\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"a-business-view-of-modernization\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#a-business-view-of-modernization\" title=\"A Business View of Modernization\"\u003eA Business View of Modernization\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModernization is not a technology upgrade — it\u0026rsquo;s a \u003cstrong\u003erisk trade-off\u003c/strong\u003e.\nThe right question isn\u0026rsquo;t \u003cem\u003e\u0026ldquo;What will it cost to rebuild?\u0026rdquo;\u003c/em\u003e but \u003cem\u003e\u0026ldquo;What will it cost if we don\u0026rsquo;t?\u0026rdquo;\u003c/em\u003e\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-economics-of-standing-still\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#the-economics-of-standing-still\" title=\"The Economics of Standing Still\"\u003eThe Economics of Standing Still\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor most medium-size .NET systems, the financial picture reveals a compelling case for action:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMaintenance Cost (Legacy):\u003c/strong\u003e ~€180 k/year\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMigration Effort:\u003c/strong\u003e ~€600–700 k one-time\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePost-Migration Cost:\u003c/strong\u003e ~€110–130 k/year\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAverage Payback:\u003c/strong\u003e ~3–4 years\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThose are directional numbers — but they tell a story.\nEach year of delay increases the risk of outage, compliance findings, and missed releases.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"hidden-multipliers\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#hidden-multipliers\" title=\"Hidden Multipliers\"\u003eHidden Multipliers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe visible costs represent only the baseline. Legacy systems create cascading inefficiencies that compound over time:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDeveloper Productivity Loss:\u003c/strong\u003e Teams spend \u003cstrong\u003e60–70% of their time\u003c/strong\u003e on maintenance tasks rather than feature development. This translates to roughly €40–60k annually in lost opportunity per developer on legacy codebases.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSecurity and Compliance Gaps:\u003c/strong\u003e Outdated frameworks often lack modern security features. Organizations typically face \u003cstrong\u003e2–3× higher audit costs\u003c/strong\u003e and increased exposure to penalties. A single compliance violation can cost €25–100k in fines and remediation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePlatform Lock-in Penalties:\u003c/strong\u003e Legacy systems often run on expensive, proprietary infrastructure. Cloud migration savings of \u003cstrong\u003e20–35%\u003c/strong\u003e remain inaccessible while maintaining legacy dependencies.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-compound-interest-of-technical-debt\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#the-compound-interest-of-technical-debt\" title=\"The Compound Interest of Technical Debt\"\u003eThe Compound Interest of Technical Debt\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnlike financial debt, technical debt doesn\u0026rsquo;t have a fixed interest rate — it accelerates. A legacy system that costs €180k to maintain today will likely cost €220–250k within three years without intervention.\u003c/p\u003e\n\u003cp\u003eMeanwhile, the migration cost remains relatively stable, but the gap between legacy and modern operational costs widens:\u003c/p\u003e\n\u003ctable class=\"striped\"\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eYear\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eLegacy Annual Cost\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eModern Annual Cost\u003c/th\u003e\n\t\t\t\t\t\u003cth style=\"text-align: left\"\u003eCumulative Savings (Post-Migration)\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€180k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€120k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€60k\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€200k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€125k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€135k\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€225k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€130k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€230k\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€250k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€135k\u003c/td\u003e\n\t\t\t\t\t\u003ctd style=\"text-align: left\"\u003e€345k\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\n\n\u003ch3 id=\"risk-quantification\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#risk-quantification\" title=\"Risk Quantification\"\u003eRisk Quantification\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBeyond operational costs, legacy systems carry measurable business risks:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDowntime Exposure:\u003c/strong\u003e Legacy systems typically experience \u003cstrong\u003e40–60% more unplanned outages\u003c/strong\u003e than modern architectures. For revenue-critical applications, each hour of downtime can cost €5–25k.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTalent Flight Risk:\u003c/strong\u003e Organizations running predominantly legacy stacks report \u003cstrong\u003e25–40% higher developer turnover\u003c/strong\u003e. Replacement and training costs average €35–50k per departing developer.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMarket Responsiveness:\u003c/strong\u003e Legacy systems constrain feature delivery speed. Companies report \u003cstrong\u003e6–12 month delays\u003c/strong\u003e in competitive responses due to legacy technical constraints.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-strategic-threshold\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#the-strategic-threshold\" title=\"The Strategic Threshold\"\u003eThe Strategic Threshold\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe modernization decision becomes clear when viewing it through a portfolio lens. Legacy systems consuming more than \u003cstrong\u003e30% of IT budget\u003c/strong\u003e while delivering less than \u003cstrong\u003e20% of business value\u003c/strong\u003e represent a strategic inflection point.\u003c/p\u003e\n\u003cp\u003eAt this threshold, every dollar invested in modernization generates measurable returns in:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eReduced operational overhead\u003c/li\u003e\n\u003cli\u003eImproved development velocity\u003c/li\u003e\n\u003cli\u003eEnhanced security posture\u003c/li\u003e\n\u003cli\u003eIncreased platform flexibility\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe question isn\u0026rsquo;t whether to modernize — it\u0026rsquo;s whether you can afford not to.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"from-tight-coupling-to-composable-systems\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#from-tight-coupling-to-composable-systems\" title=\"From Tight Coupling to Composable Systems\"\u003eFrom Tight Coupling to Composable Systems\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLegacy applications often follow a tightly coupled monolithic pattern where everything is hardwired together.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Legacy .NET Framework approach\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderController\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eController\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eActionResult\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003epayment\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentService\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eemail\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEmailService\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eSend\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomerEmail\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eView\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis creates dependencies that can\u0026rsquo;t be tested, swapped, or deployed independently. Changing the email provider means editing and redeploying the entire application.\u003c/p\u003e\n\u003cp\u003eA modern .NET approach separates these concerns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Modern .NET 8 approach\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMapPost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/orders/{id}/process\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIOrderService\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBadRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eOrderService\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIOrderService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIPaymentClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_payment\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eIEmailClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_email\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eGetOrderAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_payment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_email\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendConfirmationAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe improvement is measurable: teams report \u003cstrong\u003e30-40% faster delivery\u003c/strong\u003e once dependencies can be developed and deployed separately.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-modernization-steps\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#practical-modernization-steps\" title=\"Practical Modernization Steps\"\u003ePractical Modernization Steps\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModernization works best as incremental improvements, not big rewrites:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"1-wrap-legacy-code\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#1-wrap-legacy-code\" title=\"1. Wrap Legacy Code\"\u003e1. Wrap Legacy Code\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePut APIs around existing functionality to stop it from spreading:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Simple wrapper approach\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMapPost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/legacy/orders\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"n\"\u003eLegacyOrderSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ereq\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToLegacyFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"2-replace-piece-by-piece\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#2-replace-piece-by-piece\" title=\"2. Replace Piece by Piece\"\u003e2. Replace Piece by Piece\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eGradually swap old components for new ones:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Route to modern or legacy based on feature flags\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eprocessor\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003euseModern\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003emodernOrderProcessor\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003elegacyOrderProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eprocessor\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcessAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"3-remove-whats-left\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#3-remove-whats-left\" title=\"3. Remove What\u0026rsquo;s Left\"\u003e3. Remove What\u0026rsquo;s Left\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDecommission the old system once everything valuable has been extracted.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-real-benefits\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#the-real-benefits\" title=\"The Real Benefits\"\u003eThe Real Benefits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eModernization delivers measurable business value:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDeployment speed:\u003c/strong\u003e From hours to minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBug fixes:\u003c/strong\u003e 40% fewer defects in modern codebases\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDeveloper productivity:\u003c/strong\u003e Teams spend 60% more time on features vs. maintenance\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInfrastructure costs:\u003c/strong\u003e 20-30% savings through cloud-native patterns\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"the-choice-is-clear\"\u003e\u003ca href=\"/posts/retiring-legacy-dotnet-projects/#the-choice-is-clear\" title=\"The Choice is Clear\"\u003eThe Choice is Clear\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLegacy systems consume resources without creating value. Every month of delay makes modernization more expensive and risky.\u003c/p\u003e\n\u003cp\u003eThe goal isn\u0026rsquo;t perfect code — it\u0026rsquo;s \u003cstrong\u003esustainable progress\u003c/strong\u003e. Modern .NET gives you the tools to build systems that adapt, scale, and evolve with your business needs.\u003c/p\u003e\n\u003cp\u003eBecause maintaining the past shouldn\u0026rsquo;t prevent you from building the future.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-13T11:30:00+02:00","id":"https://daily-devops.net/posts/retiring-legacy-dotnet-projects/","language":"en","summary":"Modernize legacy .NET systems with modular architecture, risk reduction, cost efficiency strategies, and practical patterns for measurable impact.","tags":["architecture","bestpractices","dotnet","rcda","softwareengineering"],"title":"Retiring Legacy .NET Projects: Risk, Cost, Forward Motion","url":"https://daily-devops.net/posts/retiring-legacy-dotnet-projects/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the .NET ecosystem, few things have remained as stable as the unit testing landscape.\nFor years, \u003cstrong\u003exUnit\u003c/strong\u003e, \u003cstrong\u003eNUnit\u003c/strong\u003e, and \u003cstrong\u003eMSTest\u003c/strong\u003e have been the go-to frameworks — dependable, predictable, and well-integrated.\nNow, \u003cstrong\u003eTUnit\u003c/strong\u003e, a new open-source project from the community (not Microsoft), is challenging the status quo with a modern design built on source generation, concurrency, and native AOT support.\u003c/p\u003e\n\u003cp\u003eThe question isn’t whether it’s new — it’s whether it’s worth adopting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-testing-landscape-stability-meets-disruption\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#the-testing-landscape-stability-meets-disruption\" title=\"The Testing Landscape: Stability Meets Disruption\"\u003eThe Testing Landscape: Stability Meets Disruption\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost enterprise .NET teams rely on mature testing stacks that have proven themselves through countless CI/CD cycles.\nEach of the established frameworks has its place:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e – The traditional, Microsoft-endorsed option, tightly integrated into Visual Studio and Azure DevOps; predictable and enterprise-friendly, though somewhat dated in syntax and extensibility.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e – Feature-rich and stable, ideal for complex testing scenarios and broad legacy support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e – Modern conventions, parallelization by default, and a cleaner programming model for test organization.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e – The newcomer, built with Roslyn source generators and a modern runtime model (using \u003cem\u003eMicrosoft.Testing.Platform\u003c/em\u003e) focused on speed, determinism, and native AOT compatibility.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe innovation TUnit offers is architectural — not syntactical. It moves responsibility from runtime to build-time, changing how tests are discovered and executed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"familiar-syntax-subtle-evolution\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#familiar-syntax-subtle-evolution\" title=\"Familiar Syntax, Subtle Evolution\"\u003eFamiliar Syntax, Subtle Evolution\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOne of TUnit’s most compelling strengths is that it feels instantly familiar to developers.\nThe syntax closely mirrors that of xUnit, minimizing friction while adding small but meaningful improvements.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"example--tunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#example--tunit\" title=\"Example — TUnit\"\u003eExample — TUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTUnit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Arguments(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Arguments(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eParameterized_Add\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DependsOn(nameof(Add_ShouldReturnSum))]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eDependentTest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCompare that to \u003cstrong\u003eMSTest\u003c/strong\u003e, \u003cstrong\u003exUnit\u003c/strong\u003e, and \u003cstrong\u003eNUnit\u003c/strong\u003e:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"mstest\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#mstest\" title=\"MSTest\"\u003eMSTest\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.VisualStudio.TestTools.UnitTesting\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[TestClass]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestMethod]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DataRow(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DataRow(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAreEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"xunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#xunit\" title=\"xUnit\"\u003exUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eXunit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Theory]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"nunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#nunit\" title=\"NUnit\"\u003eNUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eNUnit.Framework\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestCase(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestCase(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAcross these examples, the differences are subtle — but TUnit introduces compile-time discovery, dependency control, and async-aware assertions without abandoning the simplicity that makes xUnit and MSTest approachable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-and-discovery-the-compile-time-advantage\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#performance-and-discovery-the-compile-time-advantage\" title=\"Performance and Discovery: The Compile-Time Advantage\"\u003ePerformance and Discovery: The Compile-Time Advantage\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe real technical distinction lies under the surface.\nWhile MSTest, xUnit, and NUnit rely on \u003cstrong\u003ereflection\u003c/strong\u003e to discover and run tests, TUnit shifts this process to \u003cstrong\u003ecompile time\u003c/strong\u003e via Roslyn source generators.\nThat change has measurable consequences:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eFramework\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eDiscovery Model\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAvg. Startup Time\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eParallel Execution\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAOT Compatible\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eEcosystem Maturity\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.6s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLimited\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVery High\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.8s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOptional\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVery High\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003exUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.4s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDefault\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartial\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExcellent\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSource Generation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~0.9s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBuilt-in\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEmerging\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eEarly benchmarks (from \u003ca href=\"https://andrewlock.net/converting-an-xunit-project-to-tunit/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAndrew Lock, 2024\u003c/a\u003e) show discovery and execution overhead reduced by \u003cstrong\u003e15–25%\u003c/strong\u003e in mid-sized suites.\nThat’s not academic — in enterprise CI pipelines, small savings compound fast.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eExample:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e10,000 builds per week × 15 seconds saved per run → \u003cstrong\u003e41 hours saved weekly\u003c/strong\u003e.\u003cbr/\u003e\nAt $50/hour in build infrastructure costs, that’s roughly \u003cstrong\u003e$2,000 per month\u003c/strong\u003e in real value.\u003c/p\u003e\n\u003cp\u003eThis is where TUnit begins to show \u003cstrong\u003eeconomic relevance\u003c/strong\u003e — not just theoretical efficiency.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"tooling-and-ecosystem-integration\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#tooling-and-ecosystem-integration\" title=\"Tooling and Ecosystem Integration\"\u003eTooling and Ecosystem Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTooling maturity remains TUnit’s biggest hurdle.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e integrates seamlessly with Visual Studio, Azure DevOps, and corporate reporting pipelines — it’s stable, predictable, and requires zero friction.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e and \u003cstrong\u003eNUnit\u003c/strong\u003e enjoy broad support across IDEs, build systems, and test runners; they’re the de facto standards for mature teams.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e works seamlessly through the \u003ccode\u003eMicrosoft.Testing.Platform\u003c/code\u003e layer, so it integrates well with existing tools and workflows. It works in Visual Studio, other IDEs and the CLI.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor greenfield projects, this is acceptable. For enterprise ecosystems with thousands of tests, it\u0026rsquo;s currently a deal-breaker, though automatic migration tools are emerging to address this limitation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"maintainability-and-lifecycle-considerations\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#maintainability-and-lifecycle-considerations\" title=\"Maintainability and Lifecycle Considerations\"\u003eMaintainability and Lifecycle Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit’s design aligns well with modern .NET runtime evolution — it’s built for SDK-level integration and AOT compatibility.\nHowever, unlike MSTest, it doesn’t follow Microsoft’s LTS cadence, which means \u003cstrong\u003efaster iteration\u003c/strong\u003e but \u003cstrong\u003eless predictable stability\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThat’s both opportunity and risk:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e is safe but slow-moving.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit/NUnit\u003c/strong\u003e are stable and predictable.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e evolves rapidly, reflecting the latest language and SDK advances.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor teams comfortable with early adoption, that’s an advantage. For conservative enterprise stacks, it introduces change management overhead.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"adoption-guidance\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#adoption-guidance\" title=\"Adoption Guidance\"\u003eAdoption Guidance\u003c/a\u003e\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eScenario\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRecommendation\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNew .NET 10+ projects\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ Worth adopting; future-ready and performance-efficient\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePerformance-critical CI pipelines\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ Pilot candidate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eExisting MSTest/xUnit/NUnit suites\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e⚠️ Defer migration until ecosystem matures\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLong-term enterprise projects (LTS)\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e❌ Too early; lifecycle alignment uncertain\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA reasonable approach is hybrid adoption: start with new modules or performance-sensitive components, measure, and expand only if the ROI is tangible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-business-view-value-cost-risk\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#the-business-view-value-cost-risk\" title=\"The Business View: Value, Cost, Risk\"\u003eThe Business View: Value, Cost, Risk\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAt its core, the choice of testing framework is not a technical one — it’s architectural.\nThe framework defines reliability, maintainability, and operational efficiency for years.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e guarantees continuity and corporate integration — ideal where risk avoidance trumps innovation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e offers balance — modern yet stable, performant yet well-supported.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e remains feature-rich but leans toward legacy or test-heavy applications.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e pushes testing forward — faster discovery, AOT readiness, smarter concurrency — but its youth carries risk.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe decision is ultimately about \u003cem\u003etiming\u003c/em\u003e: adopting too early adds cost; adopting too late loses competitive edge.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#final-thoughts\" title=\"Final Thoughts\"\u003eFinal Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit represents the direction .NET testing is headed — toward compile-time determinism, deeper runtime integration, and minimal overhead.\nIt’s technically elegant and forward-looking, but still maturing.\u003c/p\u003e\n\u003cp\u003eFor most organizations today, the pragmatic answer is balance:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKeep \u003cstrong\u003eMSTest\u003c/strong\u003e, \u003cstrong\u003exUnit\u003c/strong\u003e, and \u003cstrong\u003eNUnit\u003c/strong\u003e where stability matters.\u003c/li\u003e\n\u003cli\u003ePilot \u003cstrong\u003eTUnit\u003c/strong\u003e where innovation pays off.\u003c/li\u003e\n\u003cli\u003eMeasure, not assume.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn short: \u003cstrong\u003eTUnit is not a replacement (yet) for all teams, but a glimpse of the future.\u003c/strong\u003e\nAnd as always in architecture, progress is best managed, not rushed.\u003c/p\u003e\n","date_modified":"2026-06-11T17:00:40+02:00","date_published":"2025-10-09T11:30:00+02:00","id":"https://daily-devops.net/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/","language":"en","summary":"A pragmatic TUnit evaluation for .NET teams - comparing performance, maintainability, and ecosystem readiness against MSTest, xUnit, and NUnit frameworks.","tags":["architecture","bestpractices","dotnet","performance","rcda","softwareengineering","testing","tunit"],"title":"TUnit — A Pragmatic Evaluation for .NET Teams\n","url":"https://daily-devops.net/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn recent weeks, I had the opportunity to support a project explicitly built around Domain Driven Design (DDD) and Domain Driven Development principles. On the surface, this project appeared highly sophisticated, leveraging trendy abstractions and contemporary buzzwords. Yet, as I dove deeper, it quickly became clear that essential development fundamentals were being neglected.\u003c/p\u003e\n\u003cp\u003eDespite its polished exterior, the project had a weak approach to managing technical debt, resulting in significant productivity losses and unnecessary team friction. Built-in analyzers—specifically crafted for .NET—were often disregarded or explicitly disabled. Instead, the team leaned on external tools plagued with false positives, adding complexity rather than clarity.\u003c/p\u003e\n\u003cp\u003eThis scenario prompts a critical question: Why do we, as software professionals, insist on complicating things unnecessarily? Why ignore integrated, purpose-built tools in favor of unreliable external ones? It’s time we refocus on the basics beneath the buzzwords, ensuring sustainable, high-quality development practices.\u003c/p\u003e\n\u003cp\u003eWhen I raised these concerns constructively, the response was discouraging silence and apparent indifference. Sadly, this scenario isn’t rare. Too often, commitment to quality gets overridden by louder voices pushing us to \u0026ldquo;just get things done.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"maintaining-quality--tools-and-techniques\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#maintaining-quality--tools-and-techniques\" title=\"Maintaining Quality – Tools and Techniques\"\u003eMaintaining Quality – Tools and Techniques\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSoftware quality is foundational, not optional. Keeping standards high and technical debt low begins with the right tools—especially integrated analyzers in .NET projects.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-integrated-analyzers-matter\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#why-integrated-analyzers-matter\" title=\"Why Integrated Analyzers Matter\"\u003eWhy Integrated Analyzers Matter\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIntegrated analyzers provide immediate, actionable feedback directly in your IDE, reducing disruptions and enhancing productivity. They catch bugs early, enforce coding standards, and ensure consistency. Unlike external analyzers, built-in tools are specifically optimized for .NET, minimizing inaccuracies and false positives.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"essential-net-analyzers\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#essential-net-analyzers\" title=\"Essential .NET Analyzers\"\u003eEssential .NET Analyzers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere are four key analyzers that every .NET project should use:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMicrosoft.CodeAnalysis.NetAnalyzers\u003c/strong\u003e (included by default)\n\u003cul\u003e\n\u003cli\u003eCatches common bugs like memory leaks\u003c/li\u003e\n\u003cli\u003eEnforces naming conventions\u003c/li\u003e\n\u003cli\u003eIdentifies security issues\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMicrosoft.VisualStudio.Threading.Analyzers\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003ePrevents async/await deadlocks\u003c/li\u003e\n\u003cli\u003eEnsures proper threading patterns\u003c/li\u003e\n\u003cli\u003eEssential for any project using async code\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRoslynator.Analyzers\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eImproves code readability\u003c/li\u003e\n\u003cli\u003eSuggests better coding patterns\u003c/li\u003e\n\u003cli\u003eHelps maintain consistent style\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeziantou.Analyzer\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eFinds performance issues in LINQ queries\u003c/li\u003e\n\u003cli\u003eIdentifies outdated API usage\u003c/li\u003e\n\u003cli\u003eCatches resource management problems\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eRemember:\u003c/strong\u003e Every warning has a purpose. Don\u0026rsquo;t ignore them—configure them thoughtfully.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWhile some warnings may initially seem trivial or frustrating, each one signals a genuine, underlying concern. Thankfully, project settings provide flexibility to balance rigor and practicality, ensuring valuable warnings don’t get buried beneath noise.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"project-settings-that-matter\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#project-settings-that-matter\" title=\"Project Settings That Matter\"\u003eProject Settings That Matter\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAnalyzers alone aren\u0026rsquo;t enough. Your project settings must enforce quality standards:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eKey Settings:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eTreatWarningsAsErrors = true\u003c/code\u003e → Fixes warnings immediately\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eWarningLevel = 4\u003c/code\u003e → Maximum compiler checks\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eAnalysisLevel = latest\u003c/code\u003e → Uses newest quality rules\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eStrategic Configuration:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUse \u003ccode\u003eNoWarn\u003c/code\u003e to suppress specific, non-critical warnings\u003c/li\u003e\n\u003cli\u003eUse \u003ccode\u003eWarningsAsErrors\u003c/code\u003e to make specific warnings critical\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eQuality requires discipline. Don\u0026rsquo;t submit pull requests with hundreds of warnings.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"ai-code-assistants--allies-or-amplifiers-of-ignorance\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#ai-code-assistants--allies-or-amplifiers-of-ignorance\" title=\"AI Code Assistants – Allies or Amplifiers of Ignorance?\"\u003eAI Code Assistants – Allies or Amplifiers of Ignorance?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhat happens when we neglect the basics? Will advanced AI code assistants rescue us, or merely magnify our negligence? AI assistants such as GitHub Copilot or Visual Studio IntelliCode are powerful, but without foundational understanding, they risk perpetuating poor practices. AI should augment our expertise, not substitute for it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-double-edged-sword-of-ai-assistance\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#the-double-edged-sword-of-ai-assistance\" title=\"The Double-Edged Sword of AI Assistance\"\u003eThe Double-Edged Sword of AI Assistance\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI code assistants excel at pattern recognition and can significantly boost productivity when used correctly. However, they also present unique challenges:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe Good:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRapid Prototyping\u003c/strong\u003e: AI can quickly generate boilerplate code, allowing developers to focus on business logic\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLearning Accelerator\u003c/strong\u003e: Exposes developers to new patterns and libraries they might not have discovered otherwise\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConsistency\u003c/strong\u003e: Helps maintain coding patterns across team members\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eThe Problematic:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eFalse Confidence\u003c/strong\u003e: Developers may trust AI-generated code without understanding its implications\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePattern Perpetuation\u003c/strong\u003e: AI learns from existing codebases, potentially amplifying bad practices if they\u0026rsquo;re prevalent in training data\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eContext Blindness\u003c/strong\u003e: AI lacks understanding of specific project constraints, architectural decisions, or business requirements\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"a-simple-example-ai-vs-analyzers\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#a-simple-example-ai-vs-analyzers\" title=\"A Simple Example: AI vs. Analyzers\"\u003eA Simple Example: AI vs. Analyzers\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eConsider this AI-suggested code:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Looks fine, but has problems\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetDataAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStringAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToUpper\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eProblems the analyzer would catch:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eMissing cancellation support\u003c/li\u003e\n\u003cli\u003eNo \u003ccode\u003eConfigureAwait(false)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eCulture-unaware string operation\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eHere\u0026rsquo;s a cleaner approach (though still room for improvement):\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Clean, analyzer-compliant code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGetDataAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCancellationToken\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStringAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eurl\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecancellationToken\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eConfigureAwait\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToUpperInvariant\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe analyzer saves you from subtle issues and potential headaches that could cause production problems.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"using-ai-responsibly\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#using-ai-responsibly\" title=\"Using AI Responsibly\"\u003eUsing AI Responsibly\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI can certainly help with quick boilerplate generation, learning new patterns, and maintaining consistency across your codebase. However, you need to watch out for the tendency to blindly trust AI suggestions, copying bad patterns from training data, or missing project-specific context that only human developers understand.\u003c/p\u003e\n\u003cp\u003eThe key is treating AI-generated code like any junior developer\u0026rsquo;s work—review it thoroughly before integration. Keep your analyzers enabled because they serve as an excellent safety net that catches AI mistakes. Most importantly, make sure you understand the code before using it, and use AI as a learning tool rather than a replacement for critical thinking.\u003c/p\u003e\n\u003cp\u003eThink of analyzers as your safety net when using AI assistance. They provide the quality guardrails that ensure AI-generated code meets your project\u0026rsquo;s standards, catching subtle issues that might otherwise slip through into production.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bottom-line\"\u003e\u003ca href=\"/posts/buzzword-driven-development/#the-bottom-line\" title=\"The Bottom Line\"\u003eThe Bottom Line\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eDon\u0026rsquo;t let trendy buzzwords distract you from the basics. Good software development isn\u0026rsquo;t about adopting the latest methodology or framework—it\u0026rsquo;s about mastering fundamental practices that have proven their worth over time.\u003c/p\u003e\n\u003cp\u003eThe foundation of quality code starts with proper analyzers that catch problems early in the development cycle. These tools, specifically designed for .NET, provide immediate feedback and prevent common mistakes before they reach production. Combined with smart project settings that enforce quality standards, they create an environment where excellence becomes the default, not the exception.\u003c/p\u003e\n\u003cp\u003eWhen we add AI assistants to this mix, they become powerful allies rather than potential sources of technical debt. With analyzer safety nets in place, we can leverage AI\u0026rsquo;s speed and pattern recognition while maintaining the quality standards our profession demands.\u003c/p\u003e\n\u003cp\u003eMaster these fundamentals first. Everything else—whether it\u0026rsquo;s Domain Driven Design, microservices, or the next big thing—is just noise without a solid foundation. Quality isn\u0026rsquo;t optional; it\u0026rsquo;s our professional responsibility to the teams we work with and the users who depend on our software.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-07-23T17:00:00+02:00","id":"https://daily-devops.net/posts/buzzword-driven-development/","language":"en","summary":"Why fundamental .NET software quality must never be sacrificed for trendy buzzwords, including recommended analyzers, settings, and practices.","tags":["ai-code-assistant","bestpractices","codequality","csharp","dotnet","nuget","softwareengineering","technicaldebt"],"title":"Buzzword-Driven Development vs. Fundamental Software Quality","url":"https://daily-devops.net/posts/buzzword-driven-development/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eArchitectural Decision Records (ADRs) capture the \u0026ldquo;why\u0026rdquo; behind your technical choices—documenting decisions, rationale, and context for future reference. They should guide teams through complex landscapes, inform new decisions, and provide clarity during audits or onboarding.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBut here\u0026rsquo;s the problem:\u003c/strong\u003e Most ADRs become digital dust collectors.\u003c/p\u003e\n\u003cp\u003eThey sit in repositories, referenced only during crisis meetings or compliance audits. Developers bypass them during daily work, and automation tools ignore them completely. The gap between architectural intent and daily practice grows wider every sprint.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-the-problem\"\u003e\u003ca href=\"/posts/instruction-by-design/#understanding-the-problem\" title=\"Understanding the Problem\"\u003eUnderstanding the Problem\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"the-core-challenge\"\u003e\u003ca href=\"/posts/instruction-by-design/#the-core-challenge\" title=\"The Core Challenge\"\u003eThe Core Challenge\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTraditional ADRs are \u003cstrong\u003epassive documentation\u003c/strong\u003e—they record what happened, but don\u0026rsquo;t actively shape what happens next:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDiscovery friction:\u003c/strong\u003e New team members must hunt through scattered documents to understand current standards\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEnforcement gaps:\u003c/strong\u003e Build systems and linters operate independently of architectural decisions\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConsistency drift:\u003c/strong\u003e Without active reinforcement, even well-documented standards gradually erode across teams\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"the-vision-adrs-that-actually-work\"\u003e\u003ca href=\"/posts/instruction-by-design/#the-vision-adrs-that-actually-work\" title=\"The Vision: ADRs That Actually Work\"\u003eThe Vision: ADRs That Actually Work\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eImagine ADRs that don\u0026rsquo;t just document decisions—they \u003cstrong\u003edrive\u003c/strong\u003e them:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eEvery architectural choice directly influences code suggestions from AI Code Assistant\u003c/li\u003e\n\u003cli\u003eNew developers instantly understand current standards through integrated guidance\u003c/li\u003e\n\u003cli\u003eAutomation systems enforce architectural decisions in real time\u003c/li\u003e\n\u003cli\u003eTeams work with consistent, up-to-date guidance embedded in their daily tools\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis isn\u0026rsquo;t just better documentation—it\u0026rsquo;s \u003cstrong\u003eoperational architecture\u003c/strong\u003e. By making ADRs machine-consumable and embedding clear instructions, they become the single source of truth that powers both human understanding and automated enforcement.\u003c/p\u003e\n\u003cp\u003eThe result? Development environments where architectural intent is always clear, actionable, and automatically aligned across every team member and tool.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-traditional-adrs-fall-short\"\u003e\u003ca href=\"/posts/instruction-by-design/#why-traditional-adrs-fall-short\" title=\"Why Traditional ADRs Fall Short\"\u003eWhy Traditional ADRs Fall Short\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe gap between architectural intent and daily practice is where most projects struggle. Traditional ADRs capture decisions brilliantly but fail to integrate them into the development workflow where they matter most.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePassive documentation:\u003c/strong\u003e ADRs become historical artifacts that developers consult only during crisis or retrospectives—if at all.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDisconnected from automation:\u003c/strong\u003e Build systems, linters, and AI tools operate independently of architectural decisions, missing opportunities to enforce standards automatically.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOnboarding friction:\u003c/strong\u003e New team members must manually discover and interpret scattered decisions, slowing their ability to contribute effectively.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInconsistent application:\u003c/strong\u003e Without active reinforcement, even well-documented decisions gradually drift or get forgotten across different teams and projects.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"the-solution-instruction-by-design\"\u003e\u003ca href=\"/posts/instruction-by-design/#the-solution-instruction-by-design\" title=\"The Solution: Instruction by Design\"\u003eThe Solution: Instruction by Design\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"instruction-by-design-from-records-to-directives\"\u003e\u003ca href=\"/posts/instruction-by-design/#instruction-by-design-from-records-to-directives\" title=\"Instruction by Design: From Records to Directives\"\u003eInstruction by Design: From Records to Directives\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe transformation begins when we stop thinking of ADRs as documentation and start treating them as executable specifications for both human and AI behavior.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMachine-consumable structure:\u003c/strong\u003e Every ADR includes structured metadata and clear instructions that AI Code Assistant can parse and apply immediately.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOperational states with meaning:\u003c/strong\u003e \u0026ldquo;Accepted\u0026rdquo; decisions become mandatory requirements, \u0026ldquo;proposed\u0026rdquo; become considerations, while \u0026ldquo;deprecated\u0026rdquo; and \u0026ldquo;superseded\u0026rdquo; trigger active avoidance patterns.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDirect workflow integration:\u003c/strong\u003e Decisions automatically influence code suggestions, review processes, and validation pipelines without manual intervention.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSingle source of truth:\u003c/strong\u003e Both developers and AI agents reference the same authoritative guidance, eliminating interpretation gaps and ensuring consistent application.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"the-ai-enforcement-layer\"\u003e\u003ca href=\"/posts/instruction-by-design/#the-ai-enforcement-layer\" title=\"The AI Enforcement Layer\"\u003eThe AI Enforcement Layer\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen architectural decisions become machine-readable, they can drive intelligent automation throughout your development process:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-markdown\" data-lang=\"markdown\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Decision References\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e*\u003c/span\u003e MUST document all decisions in \u003cspan class=\"sb\"\u003e`decisions/`\u003c/span\u003e folder using \u003cspan class=\"sb\"\u003e`templates/architecture-decision.md`\u003c/span\u003e format.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e*\u003c/span\u003e MUST treat \u0026#34;accepted\u0026#34; decisions as mandatory requirements with highest precedence.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e*\u003c/span\u003e MUST respect decision states:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003e-\u003c/span\u003e **accepted**: mandatory requirements\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003e-\u003c/span\u003e **proposed**: optional considerations\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003e-\u003c/span\u003e **deprecated**: avoid in new implementations\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003e-\u003c/span\u003e **superseded**: forbidden, follow superseding decision instead\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e*\u003c/span\u003e MUST use the \u003cspan class=\"sb\"\u003e`instructions`\u003c/span\u003e frontmatter property as primary AI guidance for each decision.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThese rules make every ADR actionable. Human or AI, your team always knows what matters most. Now the journey with your AI buddy begins with clear, actionable guidance. Without the day-to-day friction of interpreting static documents, your team can focus on what really matters: building great software.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-enhanced-adr-template-built-for-action\"\u003e\u003ca href=\"/posts/instruction-by-design/#the-enhanced-adr-template-built-for-action\" title=\"The Enhanced ADR Template: Built for Action\"\u003eThe Enhanced ADR Template: Built for Action\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe template itself is full of helpful instructions and required fields, for clarity and standardization:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-markdown\" data-lang=\"markdown\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!-- List of authors who contributed to this decision. Include full names and roles if applicable. --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eauthors:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Name Surname \u0026lt;!-- Replace with actual name --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Another Name Surname \u0026lt;!-- Add more authors as needed --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThe patterns this decision applies to. Each entry is a glob pattern that matches files affected by this decision.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eExample:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapplyTo:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e \u0026#34;**/*.cs\u0026#34;          # Applies to all C# files\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e \u0026#34;src/**/*.razor\u0026#34;   # Applies to all Blazor components in src folder\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e \u0026#34;tests/**/*.sql\u0026#34;   # Applies to all SQL files in tests folder\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapplyTo:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e \u0026#34;**/*\u0026#34; \u0026lt;!-- Replace with specific glob patterns --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!-- The date this ADR was initially created in YYYY-MM-DD format. --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecreated: YYYY-MM-DD\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThe most recent date this ADR was updated in YYYY-MM-DD format.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eIMPORTANT: Update this field whenever the decision is modified.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003elastModified: YYYY-MM-DD\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThe current state of this ADR. If superseded, include references to the superseding ADR.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eValid values: proposed, accepted, deprecated, superseded\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003estate: proposed\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eA compact AI LLM compatible definition of this decision.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThis should be a precise, structured description that AI systems can easily parse and understand.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eInclude the core decision, key rationale, and primary impact in 1-2 concise sentences.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003einstructions: |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Compact definition of the decision made and its core purpose.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Key rationale and primary impact on the project or development process.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e---\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!-- REQUIRED: Filename MUST follow the format: YYYY-MM-DD-Title (replace all spaces with hyphens) --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gh\"\u003e# Title \u0026lt;!-- A concise title that summarizes the decision. Use a format like \u0026#34;Decision: [Short Description of Decision]\u0026#34;. --\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eA brief summary of the decision. This should be a short paragraph that captures the essence of the decision made.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Context\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eProvide a detailed explanation of the problem or issue that led to this decision. Include background information, constraints, and any relevant context to help readers understand why this decision was necessary.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Decision\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eClearly state the decision made. Describe the chosen solution or approach in detail, including any specific technologies, tools, or methods involved.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Consequences\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eExplain the implications of this decision. What are the expected benefits, trade-offs, and potential risks? How will this decision impact the project or organization?\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Alternatives Considered\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eList and describe other options that were considered. For each alternative, explain why it was not chosen. Include pros and cons, feasibility, and any other relevant factors.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Related Decisions (Optional)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026lt;!--\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eProvide links or references to other ADRs that are related to this decision. Explain how they are connected and why they are relevant.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUse markdown link syntax to reference other decisions: [\u003cspan class=\"nt\"\u003eDecision Title\u003c/span\u003e](\u003cspan class=\"na\"\u003e./YYYY-MM-DD-decision-filename.md\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eIf there are no related decisions, this section may be omitted or include a note stating \u0026#34;None at this time.\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eExample:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e [\u003cspan class=\"nt\"\u003eCentralized Package Version Management\u003c/span\u003e](\u003cspan class=\"na\"\u003e./2025-07-10-centralized-package-version-management.md\u003c/span\u003e) - Related because this decision impacts how we manage dependencies\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e [\u003cspan class=\"nt\"\u003eConventional Commits\u003c/span\u003e](\u003cspan class=\"na\"\u003e./2025-07-10-conventional-commits.md\u003c/span\u003e) - This decision affects our commit message format which impacts versioning\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eWhy this structure matters:\u003c/strong\u003e\nEvery field drives your team toward actionable clarity. No more vague rationale, ambiguous decisions, or documentation drift. The template itself becomes machine-readable—AI Code Assistant can parse every element directly, while humans get the structure they need to make consistent, enforceable decisions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"concrete-example-english-as-project-language\"\u003e\u003ca href=\"/posts/instruction-by-design/#concrete-example-english-as-project-language\" title=\"Concrete Example: English as Project Language\"\u003eConcrete Example: English as Project Language\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLet’s see how this works with a real, high-impact example.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-markdown\" data-lang=\"markdown\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eauthors:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Jane Doe, Solution Architect\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e John Smith, Lead Developer\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eapplyTo:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e \u0026#34;**/*\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecreated: 2025-07-15\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003elastModified: 2025-07-15\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003estate: accepted\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003einstructions: |\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Establish English as the mandatory language for all code, documentation, comments, commit messages, and written content to ensure consistency and global accessibility.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Applies to all identifiers, configuration files, database objects, and communication using clear, professional English standards.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e---\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gh\"\u003e# Decision: English as Project Language\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAll project artifacts, including code, docs, configs, database objects, and commit messages, must use clear, professional English. This enables global collaboration, faster onboarding, and consistent reviews—by both people and AI.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Context\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eFragmented language use has slowed down onboarding, increased misunderstandings, and made collaboration harder across regions. A single language standard solves these problems.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Decision\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAll content—code, comments, documentation, configs, and communication—must be in English, using clear and professional standards.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Consequences\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gs\"\u003e**Benefits:**\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Global teams onboard faster and communicate better\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Automated tools and AI Code Assistant can parse, review, and generate content reliably\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Fewer mistakes, less rework, and smoother audits\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gs\"\u003e**Trade-offs/Risks:**\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Non-native English speakers may need support\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Existing teams may need time to adapt\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Alternatives Considered\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Local language flexibility: Increased confusion and audit risk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e Bilingual documentation: High maintenance, likely to get out of sync\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"gu\"\u003e## Related Decisions\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e-\u003c/span\u003e [\u003cspan class=\"nt\"\u003eCentralized Documentation Standards\u003c/span\u003e](\u003cspan class=\"na\"\u003e./2025-07-10-centralized-documentation-standards.md\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis ADR is a perfect example of \u003cstrong\u003eInstruction by Design\u003c/strong\u003e. It’s not just a record of a decision; it’s a directive that shapes how your team works every day. By specifying that all project artifacts must be in English, it sets clear expectations for both human developers and AI Code Assistant.\u003c/p\u003e\n\u003cp\u003eIt eliminates ambiguity, reduces friction, and ensures that everyone—regardless of their native language—can contribute effectively.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-this-matters\"\u003e\u003ca href=\"/posts/instruction-by-design/#why-this-matters\" title=\"Why This Matters\"\u003eWhy This Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis ADR is more than just a decision; it’s a \u003cstrong\u003estandard\u003c/strong\u003e that your team can rely on. It’s clear, actionable, and enforceable. By using this template, you ensure that every architectural decision is not only documented but also actively shapes your development process.\u003c/p\u003e\n\u003cp\u003eIt’s a living document that evolves with your project, guiding both human and AI agents toward consistent, high-quality outcomes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"how-ai-and-automation-put-this-to-work\"\u003e\u003ca href=\"/posts/instruction-by-design/#how-ai-and-automation-put-this-to-work\" title=\"How AI and Automation Put This to Work\"\u003eHow AI and Automation Put This to Work\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWith Instruction by Design, your ADRs become living documents that AI Code Assistant can use to guide development. Here’s how it works in practice:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAutomatic code and docs checks:\u003c/strong\u003e\nAI Code Assistant flag any non-English content and suggest improvements in real time. Always considering the ADRs as the source of truth.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eGuided pull requests:\u003c/strong\u003e\nEvery reviewer can refer to the ADR for clear, objective decisions. Without friction, they can align on expectations and requirements quickly.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eFast onboarding:\u003c/strong\u003e\nNew developers and AI agents see the language policy immediately—and know it’s enforced.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"the-bigger-picture-operationalizing-architecture\"\u003e\u003ca href=\"/posts/instruction-by-design/#the-bigger-picture-operationalizing-architecture\" title=\"The Bigger Picture: Operationalizing Architecture\"\u003eThe Bigger Picture: Operationalizing Architecture\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eInstruction by Design is about more than just better ADRs. It’s about \u003cstrong\u003eoperationalizing architecture\u003c/strong\u003e—making your architectural decisions active participants in your development process.\nBy embedding clear, actionable instructions into every ADR, you create a system where architectural intent is always clear, enforceable, and aligned with your team’s daily work.\u003c/p\u003e\n\u003cp\u003eThis approach transforms ADRs from passive records into active guides that shape both human and AI behavior. It ensures that every decision is not just documented but also \u003cstrong\u003eoperationalized\u003c/strong\u003e—driving consistent, high-quality outcomes across your development teams.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"benefits-of-instruction-by-design\"\u003e\u003ca href=\"/posts/instruction-by-design/#benefits-of-instruction-by-design\" title=\"Benefits of Instruction by Design\"\u003eBenefits of Instruction by Design\u003c/a\u003e\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eConsistency:\u003c/strong\u003e Every team member and AI agent follows the same standards, reducing drift and confusion\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClarity:\u003c/strong\u003e Clear, actionable instructions eliminate ambiguity and ensure everyone knows what’s expected\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAutomation:\u003c/strong\u003e AI Code Assistant can enforce decisions in real time, catching issues before they become problems\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEvolution:\u003c/strong\u003e As projects grow, ADRs evolve with them—ensuring that architectural intent remains clear and actionable\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"conclusion-transform-your-adrs-today\"\u003e\u003ca href=\"/posts/instruction-by-design/#conclusion-transform-your-adrs-today\" title=\"Conclusion: Transform Your ADRs Today\"\u003eConclusion: Transform Your ADRs Today\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eInstruction by Design is a game-changer for how we think about architectural decision records. By transforming ADRs into actionable, AI-ready guidance, we bridge the gap between architectural intent and daily practice.\nNo more passive documentation—now your ADRs actively shape how your teams work, ensuring consistency, clarity, and quality across every project.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDon’t just document. Operationalize.\u003c/strong\u003e - Turn your ADRs into active guides for your people and your tools—because real progress comes from decisions you actually use. \u003cstrong\u003eReady to transform your ADRs?\u003c/strong\u003e\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-07-15T10:30:00+02:00","id":"https://daily-devops.net/posts/instruction-by-design/","language":"en","summary":"Transform architectural decision records (ADRs) into actionable AI guidance for enhanced team consistency, streamlined onboarding, and automated workflows.","tags":["ai-code-assistant","architecture","bestpractices","github","github-copilot","rcda","softwareengineering","technicaldebt"],"title":"Instruction by Design: Transforming ADRs into Actionable AI Guidance","url":"https://daily-devops.net/posts/instruction-by-design/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the world of software development, there’s a recurring tension between \u003cstrong\u003ediscipline and improvisation\u003c/strong\u003e. Somewhere along that spectrum lies a phenomenon increasingly referred to as \u003cstrong\u003eVibe Coding\u003c/strong\u003e. The term evokes a style of development where engineers follow intuition and momentum rather than formal plans, processes, or design patterns.\u003c/p\u003e\n\u003cp\u003eIt’s fast, fluid, and occasionally brilliant. But is it sustainable in a .NET-based enterprise context?\u003c/p\u003e\n\u003cp\u003eLet’s examine the merits and pitfalls of Vibe Coding, with concrete examples from the .NET environment—and a proposal for when and how to use it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-is-vibe-coding\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#what-is-vibe-coding\" title=\"What Is Vibe Coding?\"\u003eWhat Is Vibe Coding?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eVibe Coding\u003c/strong\u003e refers to a spontaneous, improvisational approach to development. Instead of beginning with architecture diagrams or layered design, developers jump directly into writing code, letting their ideas evolve as they go. It’s often associated with prototyping, hackathons, or exploratory spikes.\u003c/p\u003e\n\u003cp\u003eIn .NET, this might mean spinning up an API in 15 minutes using \u003cstrong\u003eASP.NET Core Minimal APIs\u003c/strong\u003e, building UI experiments in \u003cstrong\u003eBlazor\u003c/strong\u003e, or testing LINQ expressions directly in \u003cstrong\u003eLINQPad\u003c/strong\u003e. The approach is highly creative—but it lacks formal structure.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-vibe-coding-accelerates-development\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#when-vibe-coding-accelerates-development\" title=\"When Vibe Coding Accelerates Development\"\u003eWhen Vibe Coding Accelerates Development\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"1-prototyping-apis-with-minimal-overhead\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#1-prototyping-apis-with-minimal-overhead\" title=\"1. Prototyping APIs with Minimal Overhead\"\u003e1. Prototyping APIs with Minimal Overhead\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe \u003ccode\u003eMinimal API\u003c/code\u003e template introduced in .NET 6 is practically designed for vibe-driven exploration:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eWebApplication\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eapp\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMapGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/status\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eResults\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOk\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Healthy\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor internal tools, demos, or early-stage ideation, this approach is efficient and expressive. It enables rapid iteration without over-engineering.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"2-rapid-ui-exploration-with-blazor\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#2-rapid-ui-exploration-with-blazor\" title=\"2. Rapid UI Exploration with Blazor\"\u003e2. Rapid UI Exploration with Blazor\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFront-end behavior often benefits from real-time experimentation. With Blazor (Server or WASM), developers can explore interactions, layouts, or component communication with minimal ceremony:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e \u003cspan class=\"err\"\u003e@\u003c/span\u003e\u003cspan class=\"na\"\u003eonclick\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Toggle\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003eClick me\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ebutton\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"nt\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e@(isVisible ? \u0026#34;Hello!\u0026#34; : \u0026#34;\u0026#34;)\u003cspan class=\"p\"\u003e\u0026lt;/\u003c/span\u003e\u003cspan class=\"nt\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis kind of feedback loop fosters creativity and engagement—essential when validating UI concepts.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"3-scripting-and-querying-with-linqpad\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#3-scripting-and-querying-with-linqpad\" title=\"3. Scripting and Querying with LINQPad\"\u003e3. Scripting and Querying with LINQPad\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTools like \u003ca href=\"https://www.linqpad.net/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eLINQPad\u003c/a\u003e and \u003ccode\u003edotnet-script\u003c/code\u003e offer .NET developers a sandbox for testing LINQ queries, EF Core interactions, or complex logic in isolation—ideal for exploring new libraries or debugging issues without committing code to the main solution.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-vibe-coding-falls-short\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#where-vibe-coding-falls-short\" title=\"Where Vibe Coding Falls Short\"\u003eWhere Vibe Coding Falls Short\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"1-lack-of-architectural-foundations\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#1-lack-of-architectural-foundations\" title=\"1. Lack of Architectural Foundations\"\u003e1. Lack of Architectural Foundations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA typical symptom of overextended Vibe Coding is \u003cstrong\u003eaccidental monoliths\u003c/strong\u003e. Consider a Minimal API that grows unchecked:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMapPost\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/checkout\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eOrderRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edb\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eSqlConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Data access, validation, business rules, and notifications—all in one handler.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhat begins as a prototype quickly becomes difficult to test, extend, or scale. Critical concepts like \u003cstrong\u003eseparation of concerns\u003c/strong\u003e, \u003cstrong\u003edependency injection\u003c/strong\u003e, and \u003cstrong\u003eSOLID principles\u003c/strong\u003e are often sidelined.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"2-no-formal-testing-strategy\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#2-no-formal-testing-strategy\" title=\"2. No Formal Testing Strategy\"\u003e2. No Formal Testing Strategy\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eVibe Coding frequently leads to \u0026ldquo;just try it and see\u0026rdquo; logic. But in professional environments, we need:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUnit tests with \u003ccode\u003exUnit\u003c/code\u003e or \u003ccode\u003eNUnit\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eMocks with \u003ccode\u003eMoq\u003c/code\u003e or \u003ccode\u003eFakeItEasy\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eTestable interfaces and inversion of control\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWithout tests, teams rely on manual verification or fragile assumptions—both of which impair reliability and CI/CD readiness.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"3-technical-debt-accumulation\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#3-technical-debt-accumulation\" title=\"3. Technical Debt Accumulation\"\u003e3. Technical Debt Accumulation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePerhaps the most critical long-term risk is the \u003cstrong\u003eunmanaged accumulation of technical debt\u003c/strong\u003e. In .NET systems, this often manifests as:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTight coupling between controllers and data access\u003c/li\u003e\n\u003cli\u003eHardcoded configuration logic\u003c/li\u003e\n\u003cli\u003eBusiness rules embedded directly in API endpoints\u003c/li\u003e\n\u003cli\u003eLack of documentation, test coverage, or separation of layers\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWhat starts as quick progress soon creates \u003cstrong\u003emaintenance drag\u003c/strong\u003e: each change becomes riskier, onboarding new developers becomes harder, and long-term scalability suffers. Left unchecked, such debt can outweigh the initial productivity gains of vibe-driven work.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"a-professional-compromise-from-vibes-to-value\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#a-professional-compromise-from-vibes-to-value\" title=\"A Professional Compromise: From Vibes to Value\"\u003eA Professional Compromise: From Vibes to Value\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eVibe Coding can play a \u003cstrong\u003evaluable role at the right phase of a project\u003c/strong\u003e. The key is knowing when to \u003cstrong\u003epivot from exploration to engineering\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"suggested-progression\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#suggested-progression\" title=\"Suggested Progression\"\u003eSuggested Progression\u003c/a\u003e\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003ePhase\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eApproach\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eIdeation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVibe Coding with Minimal APIs or Blazor\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eValidation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAdd test harnesses, refactor into layers\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eScaling\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIntroduce Clean Architecture, CI/CD, observability\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eMaintenance\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDocument decisions, enforce standards\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe .NET platform is particularly well-suited to this transition:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eIHostBuilder\u003c/code\u003e and \u003ccode\u003eIServiceCollection\u003c/code\u003e offer clean extensibility.\u003c/li\u003e\n\u003cli\u003eProjects can evolve toward \u003cstrong\u003eClean Architecture\u003c/strong\u003e, with layering and dependency inversion.\u003c/li\u003e\n\u003cli\u003eTesting frameworks, analyzers, and tooling integrate smoothly into existing pipelines (Azure DevOps, GitHub Actions, etc.).\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/vibe-coding-isnt-wrong-its-unfinished/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eVibe Coding isn’t wrong—it’s unfinished.\u003c/strong\u003e — It’s a useful tool in the developer’s toolbox, especially for exploration, experimentation, and early validation. But in the context of long-lived .NET solutions, it must be tempered with structure, clarity, and discipline.\u003c/p\u003e\n\u003cp\u003eUse the vibe to build momentum.\nThen build the foundation that lasts—without the burden of unplanned debt.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-05-07T12:00:00+02:00","id":"https://daily-devops.net/posts/vibe-coding-isnt-wrong-its-unfinished/","language":"en","summary":"Explore the balance between intuitive coding and structured development in .NET, examining when vibe coding helps and when it hinders project success.","tags":["softwareengineering","bestpractices","codequality","csharp","dotnet","technicaldebt","testing"],"title":"Vibe Coding in .NET: Creative Catalyst or Maintenance Risk?","url":"https://daily-devops.net/posts/vibe-coding-isnt-wrong-its-unfinished/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eWhen we activated static code analysis for the first time in one of my last projects, the overwhelming number of warnings exceeded expectations and highlighted gaps in the code. Without making any changes, the project already had a \u003cstrong\u003esignificant number of warnings\u003c/strong\u003e. After activating additional analyzers and updating some configurations, this number \u003cstrong\u003etemporarily increased dramatically\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThe high number of warnings was initially daunting, but we saw it as an opportunity to significantly improve our code quality. At first glance, it seemed easier to suppress or ignore these warnings. But as I often remind my team, \u003cstrong\u003e\u0026ldquo;The code you create is a valuable legacy, so it\u0026rsquo;s important to build it carefully.\u0026rdquo;\u003c/strong\u003e Ignoring warnings today creates obstacles for future developers—and that could very well include you six months down the line.\u003c/p\u003e\n\u003cp\u003eThis experience reinforced the importance of managing warnings and errors systematically. Let me share some of the lessons we learned, the strategies we used to tame those 60,000 warnings, and how you can apply these techniques to your own projects.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"from-chaos-to-clarity-why-warnings-matter\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#from-chaos-to-clarity-why-warnings-matter\" title=\"From Chaos to Clarity: Why Warnings Matter\"\u003eFrom Chaos to Clarity: Why Warnings Matter\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"the-cost-of-ignoring-warnings\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#the-cost-of-ignoring-warnings\" title=\"The Cost of Ignoring Warnings\"\u003eThe Cost of Ignoring Warnings\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWarnings signal potential issues, alerting us to things that might go wrong. Ignoring these warnings can lead to subtle bugs, poor maintainability, and wasted time during debugging. When a project accumulates thousands of warnings, it creates \u003cstrong\u003ewarning fatigue\u003c/strong\u003e: developers become so desensitized to them that even critical issues go unnoticed.\u003c/p\u003e\n\u003cp\u003eOur project’s warnings could be grouped into three categories:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eLegacy Code Issues\u003c/strong\u003e: Deprecated APIs and outdated practices from years of development.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAnalyzer Rules\u003c/strong\u003e: New code-quality rules introduced by Roslyn analyzers and other tools.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNullability Warnings\u003c/strong\u003e: Warnings about potential null reference exceptions after enabling nullable reference types.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eEach required a distinct approach to address.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"configuring-net-build-turning-the-tide-against-warnings\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#configuring-net-build-turning-the-tide-against-warnings\" title=\"Configuring .NET Build: Turning the Tide Against Warnings\"\u003eConfiguring .NET Build: Turning the Tide Against Warnings\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe first step in tackling warnings is understanding how to configure their behavior in .NET Build. By setting global and file-specific properties, we gained control over how warnings were treated across the project.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"global-properties-in-net-build\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#global-properties-in-net-build\" title=\"Global Properties in .NET Build\"\u003eGlobal Properties in .NET Build\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA centralized configuration helps ensure consistency across your solution. While some properties tighten the rules around warnings, others allow for flexibility where needed. Here’s how we set up critical properties in our project:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;TreatWarningsAsErrors\u0026gt;\u003c/span\u003etrue\u003cspan class=\"nt\"\u003e\u0026lt;/TreatWarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;WarningsAsErrors\u0026gt;\u003c/span\u003eCS8602;CS8604\u003cspan class=\"nt\"\u003e\u0026lt;/WarningsAsErrors\u0026gt;\u003c/span\u003e \u003cspan class=\"c\"\u003e\u0026lt;!-- Specific warnings treated as errors --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;WarningsNotAsErrors\u0026gt;\u003c/span\u003eCS1591\u003cspan class=\"nt\"\u003e\u0026lt;/WarningsNotAsErrors\u0026gt;\u003c/span\u003e \u003cspan class=\"c\"\u003e\u0026lt;!-- Exceptions for specific warnings --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;NoWarn\u0026gt;\u003c/span\u003eCS0618\u003cspan class=\"nt\"\u003e\u0026lt;/NoWarn\u0026gt;\u003c/span\u003e \u003cspan class=\"c\"\u003e\u0026lt;!-- Suppressing non-critical warnings --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eTreatWarningsAsErrors\u003c/code\u003e\u003c/strong\u003e: This global setting enforces a \u0026ldquo;no warnings allowed\u0026rdquo; policy, treating every warning as a build-breaking error. While this is great for enforcing high standards, it can be overly strict for legacy codebases.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eWarningsAsErrors\u003c/code\u003e\u003c/strong\u003e: This allows you to escalate specific warnings to errors. For example, warnings like \u003ccode\u003eCS8602\u003c/code\u003e (dereference of a possibly null reference) and \u003ccode\u003eCS8604\u003c/code\u003e (null passed as a non-nullable parameter) were prioritized as errors in our project.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eWarningsNotAsErrors\u003c/code\u003e\u003c/strong\u003e: A complementary property to \u003ccode\u003eWarningsAsErrors\u003c/code\u003e, it provides exceptions to the rule. In our case, we decided not to escalate \u003ccode\u003eCS1591\u003c/code\u003e (missing XML documentation) to an error because enforcing this across the entire project wasn’t immediately feasible.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eNoWarn\u003c/code\u003e\u003c/strong\u003e: Temporarily suppresses warnings that are acknowledged but cannot be resolved right away. For instance, \u003ccode\u003eCS0618\u003c/code\u003e (usage of deprecated APIs) was suppressed for legacy code that we plan to refactor incrementally.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCombining these properties allowed us to enforce critical standards while giving flexibility for legacy code.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"practical-strategies-for-managing-warnings\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#practical-strategies-for-managing-warnings\" title=\"Practical Strategies for Managing Warnings\"\u003ePractical Strategies for Managing Warnings\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"1-triage-and-categorize-warnings\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#1-triage-and-categorize-warnings\" title=\"1. Triage and Categorize Warnings\"\u003e1. Triage and Categorize Warnings\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNot all warnings are created equal. We divided them into:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCritical Warnings\u003c/strong\u003e: Must be resolved immediately (e.g., potential null reference exceptions).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInformational Warnings\u003c/strong\u003e: Desirable to fix but not urgent (e.g., missing XML documentation comments).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLegacy Warnings\u003c/strong\u003e: Related to outdated APIs or practices that require phased modernization.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch4 id=\"example-prioritizing-critical-warnings\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#example-prioritizing-critical-warnings\" title=\"Example: Prioritizing Critical Warnings\"\u003eExample: Prioritizing Critical Warnings\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003eCritical warnings, like nullability issues, were escalated to errors using the \u003ccode\u003eWarningsAsErrors\u003c/code\u003e property:\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;WarningsAsErrors\u0026gt;\u003c/span\u003eCS8602;CS8604\u003cspan class=\"nt\"\u003e\u0026lt;/WarningsAsErrors\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis ensured they were always addressed before a build could succeed.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"2-using-automatic-code-fixers-wisely\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#2-using-automatic-code-fixers-wisely\" title=\"2. Using Automatic Code Fixers Wisely\"\u003e2. Using Automatic Code Fixers Wisely\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eVisual Studio provides a convenient feature for resolving many warnings through \u003cstrong\u003eautomatic code fixers\u003c/strong\u003e. These tools analyze the code and offer one-click solutions for issues, such as simplifying expressions, adding missing null checks, or suppressing warnings with \u003ccode\u003e#pragma\u003c/code\u003e directives. While these fixers can save time, they must be used with caution.\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"example-applying-an-automatic-code-fix\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#example-applying-an-automatic-code-fix\" title=\"Example: Applying an Automatic Code Fix\"\u003eExample: Applying an Automatic Code Fix\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003eConsider the following nullable warning:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Warning: Possible null reference exception\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eVisual Studio might suggest adding a null-forgiving operator (\u003ccode\u003e!\u003c/code\u003e) to suppress the warning:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e!.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Suppression applied\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhile this eliminates the warning, it introduces a potential runtime exception if \u003ccode\u003ename\u003c/code\u003e is actually \u003ccode\u003enull\u003c/code\u003e. This type of fix addresses the symptom but not the root cause, leaving the code vulnerable.\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"risks-of-overusing-automatic-fixers\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#risks-of-overusing-automatic-fixers\" title=\"Risks of Overusing Automatic Fixers\"\u003eRisks of Overusing Automatic Fixers\u003c/a\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMasking Real Issues\u003c/strong\u003e: Automatic fixes often silence warnings without addressing underlying logic problems.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIntroducing Complexity\u003c/strong\u003e: Generated fixes can add unnecessary code, such as redundant null checks, making the code harder to read and maintain.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFalse Sense of Security\u003c/strong\u003e: Developers might trust that the issue is resolved, only to find that the automatic fix created new problems.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch4 id=\"best-practices-for-using-code-fixers\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#best-practices-for-using-code-fixers\" title=\"Best Practices for Using Code Fixers\"\u003eBest Practices for Using Code Fixers\u003c/a\u003e\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eReview Every Fix\u003c/strong\u003e: Treat automatic suggestions as starting points. Always evaluate whether the proposed fix aligns with your code’s intent.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCombine with Analysis\u003c/strong\u003e: Use code fixers in tandem with a clear understanding of the warning.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAvoid Blanket Suppressions\u003c/strong\u003e: If a fixer suggests suppressing a warning (e.g., adding \u003ccode\u003e#pragma warning disable\u003c/code\u003e), consider whether this is appropriate or just hiding a deeper issue.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eBy using automatic code fixers wisely, you can ensure that they improve your code’s quality rather than creating hidden risks.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-world-example-integrating-warning-management-in-cicd-pipelines\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#real-world-example-integrating-warning-management-in-cicd-pipelines\" title=\"Real-World Example: Integrating Warning Management in CI/CD Pipelines\"\u003eReal-World Example: Integrating Warning Management in CI/CD Pipelines\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOne of the most effective ways we managed warnings was by integrating warning handling into our \u003cstrong\u003eCI/CD pipeline\u003c/strong\u003e. This allowed us to enforce consistent rules across every build and ensure that no warning could slip through the cracks during deployment.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"automated-build-configuration\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#automated-build-configuration\" title=\"Automated Build Configuration\"\u003eAutomated Build Configuration\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWe configured our \u003cstrong\u003eCI pipeline\u003c/strong\u003e to treat warnings as errors, particularly for release builds. This configuration forced the team to resolve any warnings before code could be deployed, ensuring that only clean code made it to production. By doing this, we effectively ensured that our codebase maintained a high standard without relying solely on manual intervention.\u003c/p\u003e\n\u003cp\u003eHere’s how we configured the pipeline using a \u003cstrong\u003eYAML file\u003c/strong\u003e for a .NET Core project to treat warnings as errors during the build process:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003esteps\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e- \u003cspan class=\"nt\"\u003etask\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDotNetCoreCLI@2\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003einputs\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003ecommand\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;build\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nt\"\u003earguments\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;--configuration Release /p:TreatWarningsAsErrors=true\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn this setup:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eFor \u003cstrong\u003erelease builds\u003c/strong\u003e, the \u003ccode\u003eTreatWarningsAsErrors=true\u003c/code\u003e argument was specified, ensuring that the build would fail if any warning appeared.\u003c/li\u003e\n\u003cli\u003eFor \u003cstrong\u003edebug builds\u003c/strong\u003e, we chose to allow warnings, as they would not disrupt the ongoing development work but would still be tracked for later resolution.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"ensuring-consistency-across-environments\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#ensuring-consistency-across-environments\" title=\"Ensuring Consistency Across Environments\"\u003eEnsuring Consistency Across Environments\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBy enforcing these settings in the pipeline, we ensured that no matter who worked on the code, whether locally or remotely, the same strict rules were applied. This helped prevent situations where developers ignored warnings during their local builds but let them accumulate over time, only to be caught late in the development cycle.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"continuous-monitoring-and-refinement\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#continuous-monitoring-and-refinement\" title=\"Continuous Monitoring and Refinement\"\u003eContinuous Monitoring and Refinement\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAs part of our ongoing integration process, we continually refined the warning rules based on feedback and evolving project needs. We also configured the pipeline to provide detailed reports on warnings and errors, which could be easily reviewed by the team. This helped us identify patterns or areas that required more attention, such as recurring issues with nullability or outdated API usage.\u003c/p\u003e\n\u003cp\u003eBy integrating warning management into our CI/CD pipeline, we automated and enforced quality standards across the board. This shift not only improved the code’s stability but also created a more accountable and transparent development process.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts-building-a-legacy\"\u003e\u003ca href=\"/posts/managing-errors-warnings-and-configurations/#final-thoughts-building-a-legacy\" title=\"Final Thoughts: Building a Legacy\"\u003eFinal Thoughts: Building a Legacy\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAs software developers, the code we write today becomes the foundation for future teams—or for ourselves. Ignoring warnings and errors undermines that foundation. By managing them effectively, we leave behind a valuable legacy of maintainable, high-quality code.\u003c/p\u003e\n\u003cp\u003eThrough the measures we implemented, including integrating warning management into our CI/CD pipeline, we were able to address a number of previously unknown issues. Many bugs that had quietly lurked in the codebase were brought to light and resolved—issues that had been hidden under the surface and hadn\u0026rsquo;t surfaced until we made the handling of warnings and errors a priority. Some of these bugs were revealed through the warnings themselves, while others came to light as we reviewed log files during builds and deployments.\u003c/p\u003e\n\u003cp\u003eThis process reinforced a crucial point: warnings are not just noise. They often signal deeper issues that need to be resolved before they cause significant problems down the road.\u003c/p\u003e\n\u003cp\u003eWhile we may never completely rid our projects of warnings, the key is \u003cstrong\u003eto manage them effectively\u003c/strong\u003e—and in doing so, create cleaner, more maintainable code that will stand the test of time.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2024-12-23T16:00:00+01:00","id":"https://daily-devops.net/posts/managing-errors-warnings-and-configurations/","language":"en","summary":"Learn strategies for managing static code analysis warnings, improving code quality, configuring analyzers, and integrating into CI/CD pipelines.","tags":["msbuild","bestpractices","codequality","csharp","dotnet","softwareengineering","technicaldebt"],"title":"Managing Errors, Warnings, and Configurations in C# and .NET","url":"https://daily-devops.net/posts/managing-errors-warnings-and-configurations/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn software development, there’s a silent debt that accrues interest over time, often hidden beneath layers of code and decisions made in haste or ignorance. This debt is aptly termed \u003cem\u003etechnical debt\u003c/em\u003e. Much like the german proverb, \u003cem\u003e\u0026ldquo;Wer den Pfennig nicht ehrt, ist den Taler nicht wert\u0026rdquo;,\u003c/em\u003e (or the english equivalent, \u003cem\u003e\u0026ldquo;A penny saved is a penny earned\u0026rdquo;\u003c/em\u003e) technical debt reminds us that small oversights or compromises in the present can snowball into significant challenges down the road. This article critically examines the parallels between financial principles and technical debt, emphasizing the importance of addressing both direct and indirect debt while understanding its distinction from external risks such as hacking or abuse.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-technical-debt-an-analogy-to-finance\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#understanding-technical-debt-an-analogy-to-finance\" title=\"Understanding Technical Debt: An Analogy to Finance\"\u003eUnderstanding Technical Debt: An Analogy to Finance\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAt its core, technical debt is a metaphor borrowed from finance. When developers take shortcuts—perhaps by writing suboptimal code or delaying refactoring—they incur a \u0026ldquo;debt\u0026rdquo; that must eventually be \u0026ldquo;repaid\u0026rdquo; through additional effort, time, and resources. Like monetary debt, technical debt accumulates interest in the form of maintenance overhead, slower development cycles, and reduced system stability.\u003c/p\u003e\n\u003cp\u003eIn financial terms, there are two types of debt:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGood Debt\u003c/strong\u003e: Investments like mortgages or education loans, where borrowing yields long-term benefits.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBad Debt\u003c/strong\u003e: High-interest loans or credit card balances, where borrowing becomes a perpetual burden.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eSimilarly, technical debt can be intentional or unintentional:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eIntentional Technical Debt\u003c/strong\u003e: Decisions made knowingly to meet deadlines or prioritize feature delivery. This is akin to taking a calculated loan with the intention to repay soon.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUnintentional Technical Debt\u003c/strong\u003e: Debt accrued due to lack of knowledge, poor design, or inadequate code reviews. This resembles bad debt—unplanned and harmful over time.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"intentional-vs-unintentional-technical-debt\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#intentional-vs-unintentional-technical-debt\" title=\"Intentional vs. Unintentional Technical Debt\"\u003eIntentional vs. Unintentional Technical Debt\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNot all technical debt is created equal. To fully grasp its impact, it’s critical to differentiate between \u003cstrong\u003eintentional\u003c/strong\u003e and \u003cstrong\u003eunintentional\u003c/strong\u003e technical debt.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"intentional-technical-debt\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#intentional-technical-debt\" title=\"Intentional Technical Debt\"\u003eIntentional Technical Debt\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis is the visible and measurable debt—the code shortcuts, hardcoded values, or outdated libraries. Developers know it exists and can point to it with precision. Examples include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eSkipping unit tests to deliver a feature faster.\u003c/li\u003e\n\u003cli\u003eWriting non-optimized SQL queries.\u003c/li\u003e\n\u003cli\u003eUsing deprecated APIs for quicker implementation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eDirect technical debt is like borrowing a small sum with a clear repayment plan. The problem arises when repayment is delayed, leading to compounding interest.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"indirect-technical-debt\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#indirect-technical-debt\" title=\"Indirect Technical Debt\"\u003eIndirect Technical Debt\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis is the hidden debt that manifests indirectly over time, often as a consequence of direct debt or systemic issues. Examples include:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePoorly documented code leading to knowledge silos.\u003c/li\u003e\n\u003cli\u003eOutdated infrastructure that becomes harder to replace.\u003c/li\u003e\n\u003cli\u003eAccumulated complexity that slows innovation.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIndirect debt is insidious—it’s harder to quantify and often only becomes apparent when the system begins to falter.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-compound-interest-effect-in-technical-debt\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#the-compound-interest-effect-in-technical-debt\" title=\"The Compound Interest Effect in Technical Debt\"\u003eThe Compound Interest Effect in Technical Debt\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eA defining feature of both financial and technical debt is \u003cem\u003ecompound interest\u003c/em\u003e. In software, this translates to the exponential growth of effort required to address issues as they remain unresolved.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"financial-analogy-the-power-of-compound-interest\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#financial-analogy-the-power-of-compound-interest\" title=\"Financial Analogy: The Power of Compound Interest\"\u003eFinancial Analogy: The Power of Compound Interest\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn finance, compound interest is a double-edged sword. For savings, it’s a wealth generator. For debt, it’s a destroyer. A $1,000 credit card balance at 20% annual interest, left unpaid, grows to over $6,000 in just 10 years.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"technical-debts-compound-interest\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#technical-debts-compound-interest\" title=\"Technical Debt’s Compound Interest\"\u003eTechnical Debt’s Compound Interest\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn technical systems, unresolved debt compounds in the following ways:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eIncreased Maintenance Costs\u003c/strong\u003e: Every new feature or bug fix becomes harder to implement in a convoluted codebase.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTeam Productivity Decline\u003c/strong\u003e: Developers spend more time deciphering old code instead of writing new features.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHigher Failure Risk\u003c/strong\u003e: Overloaded systems are more prone to bugs and outages.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFor instance, ignoring outdated dependencies today might seem trivial, but in a year, these dependencies could cause compatibility issues that require a complete system overhaul.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-financial-mindset-paying-off-debt-wisely\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#the-financial-mindset-paying-off-debt-wisely\" title=\"The Financial Mindset: Paying Off Debt Wisely\"\u003eThe Financial Mindset: Paying Off Debt Wisely\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTo manage technical debt effectively, developers and stakeholders need to adopt a financial mindset, considering concepts like \u003cstrong\u003eamortization\u003c/strong\u003e, \u003cstrong\u003eprincipal repayment\u003c/strong\u003e, and \u003cstrong\u003erisk assessment\u003c/strong\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"amortization-gradual-repayment\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#amortization-gradual-repayment\" title=\"Amortization: Gradual Repayment\"\u003eAmortization: Gradual Repayment\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAmortization is the process of gradually paying off a debt over time. In technical debt terms, this means allocating time in each sprint or release to tackle existing debt. For example:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePrincipal\u003c/strong\u003e: Refactor key modules incrementally.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInterest\u003c/strong\u003e: Address bugs and performance issues caused by the debt.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"cost-benefit-analysis\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#cost-benefit-analysis\" title=\"Cost-Benefit Analysis\"\u003eCost-Benefit Analysis\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEvery debt repayment decision should involve a cost-benefit analysis. Ask questions like:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhat’s the effort required to fix this debt?\u003c/li\u003e\n\u003cli\u003eWhat’s the risk of leaving it unresolved?\u003c/li\u003e\n\u003cli\u003eWill repaying it now unlock future opportunities?\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"debt-consolidation-strategic-prioritization\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#debt-consolidation-strategic-prioritization\" title=\"Debt Consolidation: Strategic Prioritization\"\u003eDebt Consolidation: Strategic Prioritization\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn finance, consolidating loans simplifies repayment. Similarly, technical debt can be \u0026ldquo;consolidated\u0026rdquo; by identifying the most critical areas to address first. Focus on high-impact debt—areas where small fixes can yield significant improvements.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"external-risks-are-not-technical-debt\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#external-risks-are-not-technical-debt\" title=\"External Risks Are Not Technical Debt\"\u003eExternal Risks Are Not Technical Debt\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIt’s essential to distinguish technical debt from external risks such as hacking, misuse, or other security vulnerabilities. While they may share some consequences, the root causes and solutions differ.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"differences-in-scope\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#differences-in-scope\" title=\"Differences in Scope\"\u003eDifferences in Scope\u003c/a\u003e\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003eAspect\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003eTechnical Debt\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003eExternal Risks\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eOrigin\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eInternal decisions or shortcuts\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExternal threats or bad actors\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eControl\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFully within the development team’s control\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartially or entirely external\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMitigation\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRefactoring, tests, documentation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSecurity protocols, firewalls, monitoring\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\n\n\u003ch3 id=\"overlap-when-risks-become-debt\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#overlap-when-risks-become-debt\" title=\"Overlap: When Risks Become Debt\"\u003eOverlap: When Risks Become Debt\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eOccasionally, external risks can create technical debt. For example, failing to patch a known vulnerability due to resource constraints incurs a debt that compounds if the system is exploited.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"a-self-reflective-look-where-we-fall-short\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#a-self-reflective-look-where-we-fall-short\" title=\"A Self-Reflective Look: Where We Fall Short\"\u003eA Self-Reflective Look: Where We Fall Short\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAs developers, we often rationalize technical debt. We promise to revisit a quick fix later or assume that future teams will handle the mess we leave behind. These assumptions are rarely true. In reality:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eShort-Term Thinking Prevails\u003c/strong\u003e: Deadlines often take precedence over quality, leading to rushed decisions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDebt Is Underestimated\u003c/strong\u003e: Teams often misjudge the time and effort required to repay debt.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStakeholders Lack Awareness\u003c/strong\u003e: Non-technical stakeholders may not understand the implications of debt, leading to underinvestment in addressing it.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis self-critique is not to assign blame but to encourage accountability. We must recognize our role in creating and perpetuating debt, as well as our power to mitigate it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"honoring-the-penny-practical-steps-forward\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#honoring-the-penny-practical-steps-forward\" title=\"Honoring the Penny: Practical Steps Forward\"\u003eHonoring the Penny: Practical Steps Forward\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTo honor the \u0026ldquo;penny\u0026rdquo; of technical debt and avoid losing the \u0026ldquo;dollar\u0026rdquo; of system stability, consider the following practices:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eTrack Debt Transparently\u003c/strong\u003e: Use tools to log and prioritize technical debt alongside feature development.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eImplement Governance\u003c/strong\u003e: Establish policies for code quality, testing, and documentation to minimize unintentional debt.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEducate Stakeholders\u003c/strong\u003e: Communicate the cost of debt in terms stakeholders understand—time, money, and risk.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCelebrate Refactoring\u003c/strong\u003e: Make debt repayment a visible and celebrated part of your team’s work.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAutomate Debt Detection\u003c/strong\u003e: Use static analysis tools to identify debt early in the development process.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEncourage Ownership\u003c/strong\u003e: Empower developers and operations to take responsibility for the debt they create and resolve it proactively.\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/tale-of-forgotten-pennies-and-lost-dollars/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe proverb \u003cem\u003e\u0026ldquo;Wer den Pfennig nicht ehrt, ist den Taler nicht wert\u0026rdquo;\u003c/em\u003e teaches us the value of small, consistent actions. In software development, this wisdom is crucial for managing technical debt. By respecting the pennies—addressing small issues promptly and intentionally—we can avoid the compound interest that turns minor debts into major crises.\u003c/p\u003e\n\u003cp\u003eAs stewards of our systems, let us commit to honoring the pennies of our craft, ensuring that our codebases remain worthy of the dollars they aim to generate.\u003c/p\u003e\n","date_modified":"2026-05-25T23:10:21+02:00","date_published":"2024-11-22T16:45:00+01:00","id":"https://daily-devops.net/posts/tale-of-forgotten-pennies-and-lost-dollars/","language":"en","summary":"Discover how small technical debts accumulate into major project costs and learn strategies to manage them effectively in software development.","tags":["technicaldebt","bestpractices","dependency-management","rcda","softwareengineering"],"title":"A Tale of Forgotten Pennies and Lost Dollars","url":"https://daily-devops.net/posts/tale-of-forgotten-pennies-and-lost-dollars/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eAs developers, we’re often tasked with maintaining and modernizing legacy codebases that were written long before some of the best practices of today—such as nullability annotations—were available. While modern C# now supports nullable reference types, enabling us to avoid the dreaded \u003ccode\u003eNullReferenceException\u003c/code\u003e, introducing this feature to existing, large codebases can be a challenge.\u003c/p\u003e\n\u003cp\u003eIn this article, I’ll share my step-by-step approach for introducing nullability into a legacy .NET and C# project. You’ll learn how to apply nullability in a controlled, incremental manner using project-level settings, scoped annotations, and file/method-level directives, all while maintaining the integrity of your legacy codebase. After all, modernizing your code doesn’t have to be an all-or-nothing endeavor—gradual change is key to a successful transition. Let’s get started!\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-gradually-introduce-nullability\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#why-gradually-introduce-nullability\" title=\"Why Gradually Introduce Nullability?\"\u003eWhy Gradually Introduce Nullability?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNullability annotations in C# allow us to specify whether a reference type can be \u003ccode\u003enull\u003c/code\u003e or not. This feature brings more type safety and reliability to your code, reducing the chance of runtime errors caused by \u003ccode\u003enull\u003c/code\u003e values. But here’s the challenge: introducing nullability into an existing, possibly large codebase and no clear code style, where methods and properties might be riddled with potential \u003ccode\u003enull\u003c/code\u003es, can result in an overwhelming number of compiler warnings. In such cases, it\u0026rsquo;s easy to give up on nullability altogether, leaving your codebase vulnerable to null reference exceptions. But it doesn\u0026rsquo;t have to be that way.\u003c/p\u003e\n\u003cp\u003eTo address this, you can take an \u003cstrong\u003eincremental approach\u003c/strong\u003e. Rather than trying to make your entire codebase \u003ccode\u003enull\u003c/code\u003e-safe in one go, you can introduce nullability \u003cstrong\u003estep by step\u003c/strong\u003e—starting with new code and gradually refactoring old code. This method minimizes disruption and lets your team handle the transition without being flooded with warnings.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"step-1-understanding-nullability-annotations-in-c\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#step-1-understanding-nullability-annotations-in-c\" title=\"Step 1: Understanding Nullability Annotations in C#\"\u003eStep 1: Understanding Nullability Annotations in C#\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIn modern C#, a \u003ccode\u003estring\u003c/code\u003e is assumed \u003cstrong\u003enot nullable\u003c/strong\u003e by default, meaning it cannot contain a \u003ccode\u003enull\u003c/code\u003e value without a compiler warning. However, you can explicitly declare a \u003ccode\u003estring\u003c/code\u003e as nullable by using the \u003ccode\u003estring?\u003c/code\u003e syntax. Here’s an example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003enonNullableString\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Hello\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// Can\u0026#39;t be null, compiler will warn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003enullableString\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// Can be null, no compiler warning\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe nullable \u003ccode\u003estring?\u003c/code\u003e type indicates that the variable may contain a \u003ccode\u003enull\u003c/code\u003e value, while the non-nullable \u003ccode\u003estring\u003c/code\u003e enforces that \u003ccode\u003enull\u003c/code\u003e values are not allowed.\u003c/p\u003e\n\u003cp\u003eThe beauty of nullable reference types is that they make your intent clear, and C#’s compiler will help enforce this through warnings whenever there’s a potential for \u003ccode\u003enull\u003c/code\u003e dereferencing. And there is a huge list of possible nullable warnings, see \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNullable reference types warnings\u003c/a\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"step-2-enabling-nullability-at-the-project-level\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#step-2-enabling-nullability-at-the-project-level\" title=\"Step 2: Enabling Nullability at the Project Level\"\u003eStep 2: Enabling Nullability at the Project Level\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe most straightforward way to introduce nullability across your entire project is by enabling it globally in your project’s \u003ccode\u003e.csproj\u003c/code\u003e file or your \u003ccode\u003eDirectory.Build.props\u003c/code\u003e file. You can do this by adding the following property inside your \u003ccode\u003e\u0026lt;PropertyGroup\u0026gt;\u003c/code\u003e section:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;Nullable\u0026gt;\u003c/span\u003eenable\u003cspan class=\"nt\"\u003e\u0026lt;/Nullable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis setting will enable nullable reference types for every file in the project, enforcing null-safety checks everywhere. However, this can result in a lot of warnings right away—especially in a large legacy codebase. If you’re not ready to tackle all these warnings at once, you might want to consider a more cautious approach.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"a-safer-option-nullableannotationsnullable\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#a-safer-option-nullableannotationsnullable\" title=\"A Safer Option: \u0026lt;Nullable\u0026gt;annotations\u0026lt;/Nullable\u0026gt;\"\u003eA Safer Option: \u003ccode\u003e\u0026lt;Nullable\u0026gt;annotations\u0026lt;/Nullable\u0026gt;\u003c/code\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eInstead of enabling nullability across the board from the start, you can take a more cautious approach by using the following property in your \u003ccode\u003e.csproj\u003c/code\u003e file:\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;PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;Nullable\u0026gt;\u003c/span\u003eannotations\u003cspan class=\"nt\"\u003e\u0026lt;/Nullable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/PropertyGroup\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis option only enables \u003cstrong\u003eannotations\u003c/strong\u003e (i.e., you can specify nullable or non-nullable reference types), but it won’t trigger warnings for potential null reference issues in your code. This way, you can begin adding annotations like \u003ccode\u003estring?\u003c/code\u003e and \u003ccode\u003eint?\u003c/code\u003e to clarify nullability without worrying about fixing warnings immediately. This is particularly useful for large legacy projects where refactoring everything at once isn\u0026rsquo;t practical.\u003c/p\u003e\n\u003cp\u003eOnce you feel confident with the annotations you’ve added, you can switch the property to \u003ccode\u003e\u0026lt;Nullable\u0026gt;enable\u0026lt;/Nullable\u0026gt;\u003c/code\u003e and start addressing the warnings generated by the compiler for potential nullability issues. But until then, you can work on adding nullability annotations at your own pace.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"step-3-using-nullable-directives-for-scoped-control\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#step-3-using-nullable-directives-for-scoped-control\" title=\"Step 3: Using #nullable Directives for Scoped Control\"\u003eStep 3: Using \u003ccode\u003e#nullable\u003c/code\u003e Directives for Scoped Control\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIn addition to enabling nullability project-wide, you may also want to control nullability at more granular levels. C# provides the \u003ccode\u003e#nullable\u003c/code\u003e directive, which allows you to enable or disable nullability checks at the file or method level. This gives you more control over where nullability is enforced, allowing you to gradually introduce null-safety to your codebase.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"file-level-nullability-control\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#file-level-nullability-control\" title=\"File-Level Nullability Control\"\u003eFile-Level Nullability Control\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you want to enable nullability for just a specific file, you can use the \u003ccode\u003e#nullable enable\u003c/code\u003e directive at the top of the file. This should be the first line in the file, before any namespaces or other code. Here’s an example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomerService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomerName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Null-safety checks enabled\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// This is allowed because \u0026#39;string?\u0026#39; is nullable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAddCustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Additional code\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn this case, all code within the file will now adhere to nullability rules. This is useful if you\u0026rsquo;re working on a new file or refactoring an older one.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"method-level-nullability-control\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#method-level-nullability-control\" title=\"Method-Level Nullability Control\"\u003eMethod-Level Nullability Control\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you only want to enable nullability for a specific method rather than an entine file, you can do so using \u003ccode\u003e#nullable\u003c/code\u003e around the method itself. This can help you focus on null-safety checks for specific methods without affecting the rest of the file. This is particularly helpful when you’re working on large files and want to incrementally refactor certain methods:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eCustomerService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003eenable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring?\u003c/span\u003e \u003cspan class=\"n\"\u003eGetCustomerName\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Null-safety checks enabled just for this method\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cp\"\u003e#nullable\u003c/span\u003e \u003cspan class=\"n\"\u003erestore\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eLegacyMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCustomer\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomer\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Null-safety checks are restored to project setting\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis way, you can enable null-safety in smaller, manageable chunks and refactor legacy methods over time. The \u003ccode\u003e#nullable restore\u003c/code\u003e directive resets the nullability setting to the project level, ensuring that the rest of the file adheres to the global nullability setting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"step-4-transitioning-to-full-nullability\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#step-4-transitioning-to-full-nullability\" title=\"Step 4: Transitioning to Full Nullability\"\u003eStep 4: Transitioning to Full Nullability\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOnce you’ve added nullability annotations throughout your code and feel confident in the accuracy of those annotations, you can switch the project setting from \u003ccode\u003e\u0026lt;Nullable\u0026gt;annotations\u0026lt;/Nullable\u0026gt;\u003c/code\u003e to \u003ccode\u003e\u0026lt;Nullable\u0026gt;enable\u0026lt;/Nullable\u0026gt;\u003c/code\u003e. This change will turn on full null-safety, meaning the compiler will now generate warnings for potential null reference issues.\u003c/p\u003e\n\u003cp\u003eAt this point, your task will be to resolve any warnings by checking for null values, using the null-coalescing operator (\u003ccode\u003e??\u003c/code\u003e), or adjusting method signatures to ensure null-safety. Gradually fixing these warnings will make your code more robust and reduce the risk of runtime null reference exceptions.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/introducing-nullability-in-legacy-code/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIntroducing nullability into a legacy codebase doesn’t have to be a daunting task. By taking an incremental approach—starting with project-level settings like \u003ccode\u003e\u0026lt;Nullable\u0026gt;annotations\u0026lt;/Nullable\u0026gt;\u003c/code\u003e, using \u003ccode\u003e#nullable\u003c/code\u003e directives for scoped control, and focusing on new or refactored code—you can modernize your codebase while avoiding the flood of warnings that comes with a full-on switch to nullability.\u003c/p\u003e\n\u003cp\u003eRemember, the goal is to improve the long-term reliability of your code, and with nullability annotations, you’re well on your way to a safer, more maintainable C# project. Take it step by step, and soon, your legacy codebase will be a thing of the past. Modern, \u003ccode\u003enull\u003c/code\u003e-safe code awaits\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2024-10-07T17:15:00+02:00","id":"https://daily-devops.net/posts/introducing-nullability-in-legacy-code/","language":"en","summary":"Step-by-step guide for implementing nullable reference types in legacy .NET and C# codebases with practical strategies, patterns, and best practices.","tags":["csharp","bestpractices","codequality","dotnet","softwareengineering"],"title":"Introducing Nullability in Legacy .NET Code","url":"https://daily-devops.net/posts/introducing-nullability-in-legacy-code/"}],"language":"en","title":"Software Engineering Principles and Practices on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}