{"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 C# Programming Language Articles on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/csharp/feed.json","home_page_url":"https://daily-devops.net/tags/csharp/","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\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\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\u003e\u0026ldquo;Just use a Service Principal,\u0026rdquo; they said. \u0026ldquo;Store the secret in Key Vault,\u0026rdquo; they said. So you did. And now that secret has been in your Git history since 2019, copied to three different environments, and nobody remembers which applications actually use it.\u003c/p\u003e\n\u003cp\u003eEvery Azure subscription I\u0026rsquo;ve worked with contains at least a dozen connection strings with embedded credentials scattered across configuration files, Key Vault secrets that still contain passwords, and Service Principal credentials checked into Git history. The credential sprawl is real. It\u0026rsquo;s not because developers are careless. It\u0026rsquo;s because the traditional authentication patterns we learned for on-premises systems don\u0026rsquo;t translate to cloud environments.\u003c/p\u003e\n\u003cp\u003eThe \u0026ldquo;create a service account, store the password somewhere secure\u0026rdquo; approach made sense when \u0026ldquo;somewhere secure\u0026rdquo; was a locked filing cabinet in the server room. In the cloud, that password ends up in CI/CD variables, Docker image layers, application logs, and that one Slack channel where someone shared the connection string \u0026ldquo;just for testing.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eAzure Managed Identity and Role-Based Access Control (RBAC) provide the solution. Not a workaround. Not a mitigation. An actual architectural pattern that makes credential leakage technically impossible for Azure resource authentication. Let\u0026rsquo;s examine why the traditional approach fails and how to implement credential-free authentication properly in .NET applications.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-pattern-credential-sprawl-in-azure\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#the-fatal-pattern-credential-sprawl-in-azure\" title=\"The Fatal Pattern: Credential Sprawl in Azure\"\u003eThe Fatal Pattern: Credential Sprawl in Azure\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eBefore we examine the correct implementation, let\u0026rsquo;s be explicit about what we\u0026rsquo;re trying to eliminate. These patterns appear in production systems daily, each representing a potential breach vector.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"service-principal-credentials-in-configuration\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#service-principal-credentials-in-configuration\" title=\"Service Principal Credentials in Configuration\"\u003eService Principal Credentials in Configuration\u003c/a\u003e\u003c/h3\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=\"c1\"\u003e// Fatal: Service Principal credentials in appsettings.json\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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;AzureAd\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;ClientId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;a1b2c3d4-e5f6-7890-abcd-ef1234567890\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;ClientSecret\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;P@ssw0rd123!SuperSecret\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;TenantId\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;12345678-90ab-cdef-1234-567890abcdef\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=\"nt\"\u003e\u0026#34;StorageAccount\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;ConnectionString\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;DefaultEndpointsProtocol=https;AccountName=mystorageacct;AccountKey=abcd1234...==\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\u003eThis configuration file contains everything an attacker needs to authenticate as your application. If this file appears in your Git history, Docker image layers, or application logs, you\u0026rsquo;ve handed over the keys to your infrastructure.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve lost count of how many times I\u0026rsquo;ve seen this exact pattern in production. The justification is always the same: \u0026ldquo;We need the credentials for local development\u0026rdquo; or \u0026ldquo;The deployment pipeline requires them.\u0026rdquo; Both are false. Both have better solutions. But the path of least resistance wins, and suddenly you have permanent credentials embedded in your codebase.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"storage-account-access-keys-in-code\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#storage-account-access-keys-in-code\" title=\"Storage Account Access Keys in Code\"\u003eStorage Account Access Keys in Code\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// Fatal: Hardcoded storage credentials\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\"\u003eDocumentService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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=\"kd\"\u003econst\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eStorageAccountKey\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;abcdef...==\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\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eUploadDocument\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eStream\u003c/span\u003e \u003cspan class=\"n\"\u003edocument\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003efileName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003ecredentials\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStorageSharedKeyCredential\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;mystorageacct\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eStorageAccountKey\u003c/span\u003e\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\"\u003eblobClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eBlobServiceClient\u003c/span\u003e\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\"\u003eUri\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://mystorageacct.blob.core.windows.net\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003ecredentials\u003c/span\u003e\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// Upload 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\u003cp\u003eStorage account access keys provide unrestricted access to all operations on all containers. They cannot be scoped. Once leaked, the only remediation is key rotation, which breaks all other applications using that key. You\u0026rsquo;re responding to a security incident by creating an availability incident.\u003c/p\u003e\n\u003cp\u003eThe irony? Storage account keys are the most commonly leaked credentials in Azure breaches. They\u0026rsquo;re also the easiest to eliminate with Managed Identity. Yet teams cling to them because \u0026ldquo;that\u0026rsquo;s how we\u0026rsquo;ve always done it.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"sql-connection-strings-with-passwords\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#sql-connection-strings-with-passwords\" title=\"SQL Connection Strings with Passwords\"\u003eSQL Connection Strings with Passwords\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// Fatal: SQL credentials in connection string\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eoptionsBuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUseSqlServer\u003c/span\u003e\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;Server=tcp:myserver.database.windows.net;Database=mydb;\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;User ID=sqladmin;Password=P@ssw0rd123!;Encrypt=true;\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\u003eSQL authentication credentials are typically shared across environments, can\u0026rsquo;t be rotated without updating every application instance, and audit logs show all activity as the same user account. You can\u0026rsquo;t trace actions to specific applications.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-pattern-managed-identity-and-rbac\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#the-correct-pattern-managed-identity-and-rbac\" title=\"The Correct Pattern: Managed Identity and RBAC\"\u003eThe Correct Pattern: Managed Identity and RBAC\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAzure Managed Identity eliminates credentials from application code by leveraging Azure AD\u0026rsquo;s OAuth 2.0 implementation. The Azure platform manages the credential lifecycle, token acquisition, and rotation automatically.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s the key insight that changes everything: instead of your application proving identity by presenting a secret (which can be stolen), Azure proves your application\u0026rsquo;s identity through platform-level cryptographic attestation. The token never leaves Azure\u0026rsquo;s infrastructure. There\u0026rsquo;s nothing to leak because there\u0026rsquo;s nothing stored in your code.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"system-assigned-managed-identity\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#system-assigned-managed-identity\" title=\"System-Assigned Managed Identity\"\u003eSystem-Assigned Managed Identity\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe simplest pattern uses a system-assigned Managed Identity, which creates a service principal tied to a specific Azure resource\u0026rsquo;s lifecycle.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bicep\" data-lang=\"bicep\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// App Service with system-assigned Managed Identity\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=\"kd\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003eappService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;Microsoft.Web/sites@2022-09-01\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e \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=\"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=\"nv\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003eappServiceName\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=\"nv\"\u003elocation\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003elocation\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=\"nv\"\u003eidentity\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=\"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=\"kd\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;SystemAssigned\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"c1\"\u003e// Creates managed identity automatically\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=\"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=\"nv\"\u003eproperties\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=\"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=\"nv\"\u003eserverFarmId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003eappServicePlan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003eid\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=\"nv\"\u003ehttpsOnly\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\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=\"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=\"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\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// RBAC: Grant least-privilege access to storage\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=\"kd\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003estorageBlobDataContributor\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;Microsoft.Authorization/roleAssignments@2022-04-01\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e \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=\"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=\"nv\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eguid\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003estorageAccount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003eappService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;ba92f5b4-2d11-453d-a403-e96b0029c9fe\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nv\"\u003escope\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003estorageAccount\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=\"nv\"\u003eproperties\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=\"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=\"c1\"\u003e// Storage Blob Data Contributor - not full Contributor!\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=\"nv\"\u003eroleDefinitionId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esubscriptionResourceId\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=\"s\"\u003e\u0026#39;Microsoft.Authorization/roleDefinitions\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;ba92f5b4-2d11-453d-a403-e96b0029c9fe\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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nv\"\u003eprincipalId\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nv\"\u003eappService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003eidentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nv\"\u003eprincipalId\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=\"nv\"\u003eprincipalType\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;ServicePrincipal\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=\"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=\"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\u003eThe identity is automatically deleted when the App Service is deleted, preventing orphaned credentials.\u003c/p\u003e\n\u003cp\u003eNotice the RBAC assignment uses \u003ccode\u003eStorage Blob Data Contributor\u003c/code\u003e, not \u003ccode\u003eContributor\u003c/code\u003e. This is the principle of least privilege in action. The application can read and write blobs, but it cannot delete the storage account, modify network rules, or access Table/Queue storage. If compromised, the blast radius is limited to blob operations on this specific storage account.\u003c/p\u003e\n\u003cp\u003eI see teams assign Contributor or Owner roles \u0026ldquo;because it\u0026rsquo;s easier.\u0026rdquo; Easier until the security audit. Easier until the breach. The few extra lines of Bicep are worth the reduced attack surface.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-application-using-defaultazurecredential\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#net-application-using-defaultazurecredential\" title=\".NET Application Using DefaultAzureCredential\"\u003e.NET Application Using DefaultAzureCredential\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe Azure SDK for .NET provides \u003ccode\u003eDefaultAzureCredential\u003c/code\u003e, which implements a credential chain that works across local development and production environments without code changes.\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\"\u003eAzure.Identity\u003c/span\u003e\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\"\u003eAzure.Storage.Blobs\u003c/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\"\u003eAzureResourceService\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eBlobServiceClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_blobServiceClient\u003c/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\"\u003eAzureResourceService\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIConfiguration\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    \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// DefaultAzureCredential tries in order:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 1. Environment variables (CI/CD)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 2. Workload Identity (AKS)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 3. Managed Identity (Azure resources)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 4. Azure CLI (local dev)\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\"\u003ecredential\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDefaultAzureCredential\u003c/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\"\u003estorageUri\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUri\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Azure:StorageAccountUri\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_blobServiceClient\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eBlobServiceClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estorageUri\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecredential\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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=\"kt\"\u003ebyte\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eDownloadDocumentAsync\u003c/span\u003e\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\"\u003econtainerName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eblobName\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\"\u003econtainer\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_blobServiceClient\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBlobContainerClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtainerName\u003c/span\u003e\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\"\u003eblob\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtainer\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetBlobClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eblobName\u003c/span\u003e\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\"\u003eresponse\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eblob\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDownloadContentAsync\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=\"n\"\u003eresponse\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\"\u003eContent\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eToArray\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eConfiguration (appsettings.json) - Notice: no credentials\u003c/strong\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;Azure\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;StorageAccountUri\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://mystorageacct.blob.core.windows.net\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;KeyVaultUri\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;https://mykeyvault.vault.azure.net\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=\"nt\"\u003e\u0026#34;ConnectionStrings\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;DefaultConnection\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Server=tcp:myserver.database.windows.net;Database=mydb;Encrypt=true;\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\u003eThe connection string contains no \u003ccode\u003eUser ID\u003c/code\u003e or \u003ccode\u003ePassword\u003c/code\u003e. Same code works locally via Azure CLI and in production via Managed Identity.\u003c/p\u003e\n\u003cp\u003eThis is the beauty of \u003ccode\u003eDefaultAzureCredential\u003c/code\u003e: zero code changes between environments. No \u003ccode\u003e#if DEBUG\u003c/code\u003e blocks. No environment-specific configuration files with different credential strategies. The SDK figures out which credential type to use based on where it\u0026rsquo;s running. Your code stays clean.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"local-development-without-credential-management\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#local-development-without-credential-management\" title=\"Local Development Without Credential Management\"\u003eLocal Development Without Credential Management\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOne of \u003ccode\u003eDefaultAzureCredential\u003c/code\u003e\u0026rsquo;s significant advantages is enabling developers to work with Azure resources locally without managing credentials in configuration files.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDevelopment workflow\u003c/strong\u003e:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDeveloper authenticates to Azure CLI: \u003ccode\u003eaz login\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eApplication uses \u003ccode\u003eDefaultAzureCredential\u003c/code\u003e, which detects Azure CLI credentials\u003c/li\u003e\n\u003cli\u003eSame code runs in production using Managed Identity\u003c/li\u003e\n\u003cli\u003eNo credentials in \u003ccode\u003eappsettings.Development.json\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eDevelopers use their individual Azure AD accounts, maintaining a proper audit trail. No shared development credentials.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"sql-database-configuration\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#sql-database-configuration\" title=\"SQL Database Configuration\"\u003eSQL Database Configuration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAzure SQL requires additional configuration to enable Managed Identity authentication:\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=\"c1\"\u003e-- Execute as Azure AD admin on the database\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eCREATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eUSER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eservice\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eEXTERNAL\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ePROVIDER\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=\"k\"\u003eALTER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eROLE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edb_datareader\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eADD\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eMEMBER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eservice\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003ename\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=\"k\"\u003eALTER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eROLE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edb_datawriter\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eADD\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eMEMBER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003eservice\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003ename\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\u003eNote: RBAC assignments can take several minutes to propagate. If you get 403 errors right after deployment, wait a few minutes or implement retry logic.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-migration-path\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#the-migration-path\" title=\"The Migration Path\"\u003eThe Migration Path\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re starting with the fatal patterns shown earlier, here\u0026rsquo;s a pragmatic migration path:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePhase 1 - Key Vault Migration\u003c/strong\u003e (Days):\nMove existing credentials to Azure Key Vault, access Key Vault using Managed Identity. This eliminates credentials from code without changing application authentication logic.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePhase 2 - Managed Identity for Azure Resources\u003c/strong\u003e (Weeks):\nConvert storage, Service Bus, and other Azure resource authentication to use Managed Identity. This is the highest-value change. Most credential leaks involve storage keys.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePhase 3 - SQL Managed Identity\u003c/strong\u003e (Weeks):\nMigrate SQL authentication to Managed Identity. Requires coordination with DBAs for permission configuration but eliminates SQL credentials.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePhase 4 - Custom RBAC Roles\u003c/strong\u003e (Months):\nReplace built-in roles with custom role definitions scoped to minimum necessary permissions. This is optimization. The security improvement from Phase 1-3 is already substantial.\u003c/p\u003e\n\u003cp\u003eThe total effort? Weeks, not months. The hardest part isn\u0026rsquo;t the technology. It\u0026rsquo;s convincing teams to abandon patterns they\u0026rsquo;ve used for years.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-objections-and-why-they-dont-hold-up\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#the-objections-and-why-they-dont-hold-up\" title=\"The Objections (And Why They Don\u0026rsquo;t Hold Up)\"\u003eThe Objections (And Why They Don\u0026rsquo;t Hold Up)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve implemented this pattern across multiple Azure environments, and the resistance is predictable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;How do we rotate credentials now?\u0026rdquo;\u003c/strong\u003e You don\u0026rsquo;t. Azure manages the credential lifecycle. The operational burden decreases while security posture improves.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;But what about local development?\u0026rdquo;\u003c/strong\u003e Solved. Azure CLI authentication. Same code, different credential provider. Developers use their individual accounts, maintaining proper audit trails.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;Our CI/CD pipeline needs credentials.\u0026rdquo;\u003c/strong\u003e Use Workload Identity Federation for GitHub Actions or Azure DevOps service connections. Still no static credentials.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;It\u0026rsquo;s too much work to migrate.\u0026rdquo;\u003c/strong\u003e More work than responding to a credential leak? More work than explaining to your CISO why production secrets are in Git history? The migration is measured in weeks. The breach response is measured in months.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve heard every excuse. None of them hold up against the fundamental reality: static credentials are a liability. Every credential you embed is a credential that can be extracted. Every secret you store is a secret that can leak.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/managed-identity-rbac-azure-resources/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe patterns shown here work in production systems today, handling millions of requests without a single credential in code or configuration. That\u0026rsquo;s not compliance theater. That\u0026rsquo;s fundamental security architecture that makes credential leakage technically impossible for Azure resource authentication.\u003c/p\u003e\n\u003cp\u003eCredentials are hazardous material. Treat them accordingly: contained, time-limited, and scoped to minimum necessary permissions. Or better yet, eliminate them entirely with Managed Identity.\u003c/p\u003e\n\u003cp\u003eYour future self (and your security team) will thank you.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-14T17:00:00+02:00","id":"https://daily-devops.net/posts/managed-identity-rbac-azure-resources/","language":"en","summary":"That ClientSecret has been in your Git history since 2019. Here's how Azure Managed Identity eliminates credentials from your .NET apps entirely.\n","tags":["security","azure","cloudnative","dotnet","csharp"],"title":"\"We Store Secrets in appsettings.json\": A Horror Story in Five Acts\n","url":"https://daily-devops.net/posts/managed-identity-rbac-azure-resources/"},{"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\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\u003eThe .NET CLI? Reliable. Boring. You run \u003ccode\u003edotnet build\u003c/code\u003e, \u003ccode\u003edotnet test\u003c/code\u003e, \u003ccode\u003edotnet publish\u003c/code\u003e, done. Real DevOps work happens in Dockerfiles, CI/CD configs, and specialized tools. The CLI does its job but was never built for actual operational workflows.\u003c/p\u003e\n\u003cp\u003e.NET 10 changes this. Four additions that sound minor but fix real problems I\u0026rsquo;ve hit in production pipelines for years: native container publishing, ephemeral tool execution, better cross-platform packaging, and machine-readable schemas. Not flashy. Not keynote material. But they\u0026rsquo;re the kind of improvements that save hours every week once you\u0026rsquo;re running them at scale.\u003c/p\u003e\n\u003cp\u003eWill they replace your current workflow? Depends on what you\u0026rsquo;re building. Let\u0026rsquo;s look at what actually changed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"built-in-container-publishing-dockerfiles-become-optional\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#built-in-container-publishing-dockerfiles-become-optional\" title=\"Built-in Container Publishing: Dockerfiles Become Optional\"\u003eBuilt-in Container Publishing: Dockerfiles Become Optional\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s start with the biggest change: \u003ccode\u003edotnet publish\u003c/code\u003e now generates container images directly. No Dockerfile needed.\u003c/p\u003e\n\u003cp\u003eYou know the drill. Write a Dockerfile (full control, maintenance hell) or use Docker Build Cloud (more dependencies, more complexity). Both work. Both suck in their own ways.\u003c/p\u003e\n\u003cp\u003e.NET 8 tried this with \u003ccode\u003eMicrosoft.NET.Build.Containers\u003c/code\u003e—opt-in, awkward, felt bolted-on. .NET 10 makes it first-class. There are still limits, but the core experience is solid.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"how-it-works\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#how-it-works\" title=\"How It Works\"\u003eHow It Works\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eOne command:\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 publish --os linux --arch x64 /t:PublishContainer\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCompiles for Linux x64. Packages as OCI container. Tags from project metadata. Pushes if you have credentials.\u003c/p\u003e\n\u003cp\u003eNo Dockerfile. No multi-stage builds. No base image debates. The CLI handles it using project defaults—works great for standard apps, questionable for edge cases.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"practical-implications\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#practical-implications\" title=\"Practical Implications\"\u003ePractical Implications\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhere this really shines: CI/CD pipelines. I\u0026rsquo;ve been maintaining GitHub Actions workflows that look like this for years:\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\"\u003eBuild\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 build --configuration Release\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\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\"\u003eBuild Docker Image\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\"\u003edocker build -t myapp:latest .\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\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\"\u003ePush to Registry\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\"\u003edocker push myapp:latest\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\u003eNow:\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\"\u003ePublish Container\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 publish --os linux --arch x64 /t:PublishContainer\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\u003eDocker\u0026rsquo;s gone. The CLI handles everything. For lightweight build agents or K8s-based CI, this cuts build times and removes a dependency I\u0026rsquo;ve been wanting to ditch.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s the trade-off—you surrender control to CLI defaults. Standard app? Perfect. Need custom base images, specific layers, or complex configs? You\u0026rsquo;re back to Dockerfiles. The CLI won\u0026rsquo;t bend that far yet.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-dockerfiles-remain-necessary\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#when-dockerfiles-remain-necessary\" title=\"When Dockerfiles Remain Necessary\"\u003eWhen Dockerfiles Remain Necessary\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDockerfiles aren\u0026rsquo;t dead—just optional for simple cases. Need custom layers, multi-stage builds, or complex runtime configs? Use Dockerfiles. For standard ASP.NET Core on Linux? CLI wins.\u003c/p\u003e\n\u003cp\u003eThe philosophy makes sense: You shouldn\u0026rsquo;t need Docker expertise to containerize a .NET app. But production systems need security scanning, vulnerability patches, compliance checks. The CLI gives you a working container. Whether it\u0026rsquo;s production-ready depends on your standards.\u003c/p\u003e\n\u003cp\u003eContainers are one piece. The other friction point in DevOps pipelines? Tool management.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"one-shot-global-tools-dotnet-tool-exec\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#one-shot-global-tools-dotnet-tool-exec\" title=\"One-Shot Global Tools: dotnet tool exec\"\u003eOne-Shot Global Tools: \u003ccode\u003edotnet tool exec\u003c/code\u003e\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eGlobal tools have been around since .NET Core 2.1. Install, then execute. Two steps, every time, and it gets messy fast in CI environments.\u003c/p\u003e\n\u003cp\u003e.NET 10 adds \u003ccode\u003edotnet tool exec\u003c/code\u003e. Run tools without installing. Like \u003ccode\u003enpx\u003c/code\u003e in Node.js.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"before\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#before\" title=\"Before\"\u003eBefore\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s what I\u0026rsquo;ve been doing in CI pipelines:\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 tool install --global dotnet-format\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edotnet format --verify-no-changes\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAlready installed? First command fails (or needs \u003ccode\u003e--ignore-failed-sources\u003c/code\u003e). Not installed? Second fails. Managing this state across multiple build agents turns into a fragile mess with conditional logic everywhere.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"now\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#now\" title=\"Now\"\u003eNow\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 10 adds \u003ccode\u003edotnet tool exec\u003c/code\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\"\u003edotnet tool \u003cspan class=\"nb\"\u003eexec\u003c/span\u003e dotnet-format -- --verify-no-changes\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFetch, run, discard. No global state. No cleanup. No version conflicts between pipeline runs.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-this-matters-for-cicd\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#why-this-matters-for-cicd\" title=\"Why This Matters for CI/CD\"\u003eWhy This Matters for CI/CD\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCI needs stateless builds. This delivers exactly that. Each run fetches the exact tool version, executes, done. No version conflicts. No pre-baking tools into build images.\u003c/p\u003e\n\u003cp\u003eWorking with minimal base images? Just run tools on-demand. No more Dockerfile layers dedicated to tool installations that bloat your images.\u003c/p\u003e\n\u003cp\u003eThe catch is download overhead per execution. For tools you run repeatedly in tight loops, add caching. The CLI doesn\u0026rsquo;t auto-cache between \u003ccode\u003eexec\u003c/code\u003e calls, so repeated executions add latency. In most scenarios, simplicity beats speed. High-frequency pipelines? Measure first.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"local-development-benefits\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#local-development-benefits\" title=\"Local Development Benefits\"\u003eLocal Development Benefits\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis isn\u0026rsquo;t just for CI. Working across multiple projects, I\u0026rsquo;ve always hated maintaining a global tool collection that inevitably ends up with version conflicts. Now I run project-specific tools without polluting my environment. Node.js devs have had this with \u003ccode\u003enpx\u003c/code\u003e for years—about time .NET caught up.\u003c/p\u003e\n\u003cp\u003eVersioning still matters. Need a specific version? Specify it or use \u003ccode\u003edotnet-tools.json\u003c/code\u003e. The CLI won\u0026rsquo;t guess.\u003c/p\u003e\n\u003cp\u003eWhile we\u0026rsquo;re talking about tools, there\u0026rsquo;s another improvement that tool authors will appreciate.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"multi-platform-global-tool-packaging\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#multi-platform-global-tool-packaging\" title=\"Multi-Platform Global Tool Packaging\"\u003eMulti-Platform Global Tool Packaging\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOlder .NET versions claimed cross-platform support for tools. Reality? Separate packages for \u003ccode\u003elinux-x64\u003c/code\u003e, \u003ccode\u003ewin-x64\u003c/code\u003e, \u003ccode\u003eosx-arm64\u003c/code\u003e. Add native dependencies and you\u0026rsquo;re in for a nightmare.\u003c/p\u003e\n\u003cp\u003e.NET 10 improves RID handling. One NuGet package, multiple platforms. The CLI resolves the right binary at runtime based on the target platform.\u003c/p\u003e\n\u003cp\u003eWorks well for simple cases. Complex native dependencies? Still rough, but better than before.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"impact-on-tool-authors\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#impact-on-tool-authors\" title=\"Impact on Tool Authors\"\u003eImpact on Tool Authors\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you maintain global tools, you can finally ship one package for all platforms. Less packaging hell, fewer user install failures due to platform mismatches.\u003c/p\u003e\n\u003cp\u003eNot revolutionary. Incremental. Should\u0026rsquo;ve been fixed years ago, but I\u0026rsquo;ll take it.\u003c/p\u003e\n\u003cp\u003eNow here\u0026rsquo;s the feature that flew completely under the radar but might end up being the most useful for automation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cli-schema-export-automation-meets-introspection\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#cli-schema-export-automation-meets-introspection\" title=\"CLI Schema Export: Automation Meets Introspection\"\u003eCLI Schema Export: Automation Meets Introspection\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMachine-readable CLI schemas. Sounds boring. Actually game-changing for tooling and automation that currently relies on parsing brittle text output.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-is-a-cli-schema\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#what-is-a-cli-schema\" title=\"What Is a CLI Schema?\"\u003eWhat Is a CLI Schema?\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIt\u0026rsquo;s structured data describing CLI commands, options, and arguments. What the CLI can execute, what parameters it accepts, how they interact.\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 --info --format json\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eJSON output: CLI capabilities, SDKs, runtimes, commands. Queryable, parseable, stable across versions.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"use-cases\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#use-cases\" title=\"Use Cases\"\u003eUse Cases\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhere this gets practical:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIDE Integration:\u003c/strong\u003e VS Code could add IntelliSense for CLI commands directly in terminal windows. They\u0026rsquo;d need to implement it first, but the foundation\u0026rsquo;s there.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCI/CD Validation:\u003c/strong\u003e Check .NET versions before your build fails halfway through. Catch environment mismatches early instead of debugging why the pipeline suddenly broke.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTooling Development:\u003c/strong\u003e Third-party tools can adapt to CLI capabilities dynamically. No more hardcoded assumptions that break when the SDK updates.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDocumentation Generation:\u003c/strong\u003e Auto-generate CLI reference docs from the schema. Always current, never stale.\u003c/p\u003e\n\u003cp\u003eThis assumes the schema stays stable across releases. So far Microsoft\u0026rsquo;s track record is decent. Worth monitoring as it matures.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"real-world-scenario\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#real-world-scenario\" title=\"Real-World Scenario\"\u003eReal-World Scenario\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s a concrete example. Your build script needs .NET 10 before it proceeds. Before, you\u0026rsquo;d parse text output from \u003ccode\u003edotnet --version\u003c/code\u003e—fragile and breaks whenever Microsoft tweaks the format.\u003c/p\u003e\n\u003cp\u003eNow:\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 --info --format json \u003cspan class=\"p\"\u003e|\u003c/span\u003e jq -r \u003cspan class=\"s1\"\u003e\u0026#39;.sdks[] | select(.version | startswith(\u0026#34;10.\u0026#34;))\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNo results? .NET 10 isn\u0026rsquo;t installed. Results? You have the exact version and installation path. Structured, reliable, resistant to cosmetic changes.\u003c/p\u003e\n\u003cp\u003eThe catch is you need \u003ccode\u003ejq\u003c/code\u003e or equivalent JSON parsing. Modern CI systems? Not a problem. Windows environments without JSON tooling pre-installed? You\u0026rsquo;ve just shifted the complexity somewhere else.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-bigger-picture\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#the-bigger-picture\" title=\"The Bigger Picture\"\u003eThe Bigger Picture\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis isn\u0026rsquo;t flashy. It\u0026rsquo;s foundational. The CLI transforms from a black box you invoke to a queryable API. That enables an entire class of tooling improvements—assuming the ecosystem actually builds them.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-these-changes-matter\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#why-these-changes-matter\" title=\"Why These Changes Matter\"\u003eWhy These Changes Matter\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNone of these features made keynotes. No press coverage. Not language features or runtime magic. But here\u0026rsquo;s what\u0026rsquo;s significant: Microsoft\u0026rsquo;s finally investing in workflow ergonomics instead of just piling on features.\u003c/p\u003e\n\u003cp\u003eDevOps teams don\u0026rsquo;t need more frameworks. We need less friction. Fewer dependencies, fewer scripts, fewer components that break when environments change. The .NET 10 CLI moves toward that—not perfectly, but noticeably.\u003c/p\u003e\n\u003cp\u003eBuilt-in containers eliminate Docker as a build dependency for standard cases. Tool exec removes global state from CI pipelines, though you pay in download overhead. Multi-platform packaging simplifies distribution when native dependencies cooperate. Schemas enable automation if you have JSON parsing infrastructure.\u003c/p\u003e\n\u003cp\u003eReal pain points addressed. Real trade-offs introduced. The CLI shifted from \u0026ldquo;good enough\u0026rdquo; to \u0026ldquo;actually designed for ops work.\u0026rdquo; It\u0026rsquo;s not finished, but the direction is right.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion-evolution-not-revolution\"\u003e\u003ca href=\"/posts/dotnet-10-cli-devops/#conclusion-evolution-not-revolution\" title=\"Conclusion: Evolution, Not Revolution\"\u003eConclusion: Evolution, Not Revolution\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET 10 isn\u0026rsquo;t revolutionary. It\u0026rsquo;s evolutionary. That\u0026rsquo;s exactly what maturing platforms need—incremental wins that compound over time.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re running DevOps pipelines, building CI/CD workflows, or maintaining .NET tooling, these features aren\u0026rsquo;t curiosities. They\u0026rsquo;re practical improvements that\u0026rsquo;ll simplify your work, speed execution, and improve reliability. You just need to understand the limits.\u003c/p\u003e\n\u003cp\u003eIn environments where every eliminated dependency multiplies across thousands of builds monthly, \u0026ldquo;simpler\u0026rdquo; becomes a legitimate feature. Whether .NET 10\u0026rsquo;s CLI improvements hit \u0026ldquo;simple enough\u0026rdquo; depends on your operational context and tolerance for trade-offs.\u003c/p\u003e\n\u003cp\u003eThe direction is right. Whether it\u0026rsquo;s sufficient for your specific needs? Test it in your environment. The CLI\u0026rsquo;s finally built for DevOps work. Time to see if it holds up to production reality.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-25T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-10-cli-devops/","language":"en","summary":".NET 10 CLI finally ships features DevOps teams needed years ago: built-in container builds, ephemeral tools, and machine-readable schemas across the SDK.","tags":["cli","dotnet","csharp","devops","bestpractices"],"title":".NET CLI 10 – Microsoft Finally Realizes DevOps Exists","url":"https://daily-devops.net/posts/dotnet-10-cli-devops/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLet me tell you what I\u0026rsquo;ve learned over the years from watching teams deploy logging strategies that looked great on paper and failed spectacularly at 3 AM when production burned.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s not that they didn\u0026rsquo;t know the theory. They\u0026rsquo;d read the Azure documentation. They\u0026rsquo;d seen the structured logging samples. They\u0026rsquo;d studied distributed tracing. The real problem was different: they knew \u003cem\u003ewhat\u003c/em\u003e to do but had no idea \u003cem\u003ewhy\u003c/em\u003e it mattered until production broke catastrophically.\u003c/p\u003e\n\u003cp\u003eThis article isn\u0026rsquo;t about generic \u0026ldquo;best practices\u0026rdquo; or theoretical frameworks. Instead, it\u0026rsquo;s about the specific, concrete ways logging strategies fail in real production systems—why teams log things that don\u0026rsquo;t actually help, miss logging things that critically do, and build expensive observability infrastructure that doesn\u0026rsquo;t deliver when it matters most.\u003c/p\u003e\n\u003cp\u003eAnd I\u0026rsquo;m quite confident that your team is already doing at least two of these things right now.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-core-problem-logging-isnt-about-logging\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#the-core-problem-logging-isnt-about-logging\" title=\"The Core Problem: Logging Isn\u0026rsquo;t About Logging\"\u003eThe Core Problem: Logging Isn\u0026rsquo;t About Logging\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s the fundamental issue: most teams approach logging in a fundamentally backward way. They start by asking themselves: \u0026ldquo;What should we log?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s completely wrong. The right question—the one that changes everything—is: \u0026ldquo;What information do we absolutely need to diagnose a production failure when everything is burning?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eBecause logging isn\u0026rsquo;t a feature. It\u0026rsquo;s insurance. And like all insurance, you want to pay the minimum premium for maximum coverage. You don\u0026rsquo;t insure against every possible outcome; you insure against the catastrophic ones.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"anti-pattern-1-logging-everything-just-in-case\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#anti-pattern-1-logging-everything-just-in-case\" title=\"Anti-Pattern 1: Logging Everything \u0026ldquo;Just in Case\u0026rdquo;\"\u003eAnti-Pattern 1: Logging Everything \u0026ldquo;Just in Case\u0026rdquo;\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve seen applications log 50+ MB per request. Developers reasoned with apparent logic: \u0026ldquo;More data = better debugging.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThis is not just wrong. It\u0026rsquo;s catastrophically wrong. And I can prove it with concrete math and real-world consequences.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe Reality of Excessive Logging\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eLet\u0026rsquo;s walk through a concrete example. Consider a typical e-commerce order processing request that touches multiple services. A well-intentioned developer adds \u0026ldquo;detailed diagnostic logging\u0026rdquo; at every single step—serializing objects, logging variable states, capturing full request/response payloads. It seems reasonable. It looks thorough. It feels safe.\u003c/p\u003e\n\u003cp\u003eThen production hits real load. Assume 100 requests per second, each with 5 MB of unfiltered diagnostic data. That\u0026rsquo;s 500 MB per second of logs flowing into your systems. Your log ingestion pipeline starts struggling. You\u0026rsquo;re either dropping logs or compressing aggressively (and losing critical detail). Your monthly storage bill—depending on your tool and retention policy—can easily escalate from a comfortable $200 to several thousand dollars. The actual impact varies depending on your setup: Application Insights charges per GB ingested, Datadog per host/span volume, Elasticsearch per GB stored. It\u0026rsquo;s not always catastrophic, but it\u0026rsquo;s significant enough to force painful cost-cutting decisions.\u003c/p\u003e\n\u003cp\u003eBut more importantly than cost, here\u0026rsquo;s what actually happens in practice:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eSearch becomes genuinely frustrating.\u003c/strong\u003e With gigabytes of noise, finding a specific error means sifting through thousands of irrelevant entries. A query for \u0026ldquo;payment timeout\u0026rdquo; returns 500 results. Which one is actually yours? You don\u0026rsquo;t know.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLogs stop being useful entirely.\u003c/strong\u003e Not because they\u0026rsquo;re stored badly, but because finding signal in the noise takes longer than just restarting the service and hoping it works. So teams gradually stop using logs for diagnosis and instead use luck.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReal problems hide effectively.\u003c/strong\u003e The actual error is there somewhere, buried in noise about every intermediate step, every variable assignment, every function entry. By the time you find it, the incident is already over and customers are angry.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eYou\u0026rsquo;re paying for data nobody uses.\u003c/strong\u003e Not $13,000/day in runaway costs, but definitely enough to notice and enough to make management ask questions.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis is exactly what happens when you optimize for \u003cem\u003ecompleteness\u003c/em\u003e instead of \u003cem\u003esignal\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe solution is surprisingly simple:\u003c/strong\u003e Log only what you\u0026rsquo;d actually need to diagnose a failure. Not what \u003cem\u003emight\u003c/em\u003e be useful someday. Not \u0026ldquo;this function was called.\u0026rdquo; Not \u0026ldquo;this variable is 42.\u0026rdquo; Only things that directly help answer: \u0026ldquo;Why did this critical operation fail?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eIn concrete terms: when an order fails, you truly need to know \u003cem\u003ewhat\u003c/em\u003e failed and \u003cem\u003ewhy\u003c/em\u003e. Did validation reject it? Did payment timeout? Did the warehouse queue overflow? Did inventory run out? Each failure mode has a completely different cause and a different fix. So you log specifically for those scenarios, not for everything in between.\u003c/p\u003e\n\u003cp\u003eA typical refactoring looks like this: instead of logging every intermediate step (retrieved order, started validation, started payment, called warehouse), you log only outcome points (order complete, order failed with specific reason X). This cuts noise by roughly 80% while actually \u003cem\u003eimproving\u003c/em\u003e diagnostic value. You know what mattered. You can find it in seconds.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"anti-pattern-2-fire-and-forget-observability\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#anti-pattern-2-fire-and-forget-observability\" title=\"Anti-Pattern 2: Fire-and-Forget Observability\"\u003eAnti-Pattern 2: Fire-and-Forget Observability\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;ve attended a cloud architecture conference. You heard talks about observability and its importance. You read the Microsoft Learn documentation on Application Insights. You diligently configured it—set up the Azure SDK, added OpenTelemetry, made sure logs flow reliably to the cloud.\u003c/p\u003e\n\u003cp\u003eYou check the box: \u0026ldquo;Observability: Done.\u0026rdquo; Problem solved, right?\u003c/p\u003e\n\u003cp\u003eThen production breaks at 2 AM. You wake up. You go to Application Insights and\u0026hellip; find nothing useful. No signal, just noise. So you deploy a quick fix with logging at DEBUG level. Now you have terabytes of noise flooding in. You restart the service and hope it doesn\u0026rsquo;t happen again. Problem \u0026ldquo;fixed\u0026rdquo; (until it does).\u003c/p\u003e\n\u003cp\u003eThis pattern happens constantly. Not because Application Insights is fundamentally bad. Not because you\u0026rsquo;re incompetent. But because observability was never actually designed for \u003cem\u003eyour specific\u003c/em\u003e application and \u003cem\u003eyour specific\u003c/em\u003e failure modes. You bought expensive tools. You installed them correctly. You patted yourself on the back. Then you walked away without thinking deeply.\u003c/p\u003e\n\u003cp\u003eObservability without genuine understanding isn\u0026rsquo;t observability. It\u0026rsquo;s just expensive logging theater—looking good in slides but useless when it matters.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReal observability requires answering three critical questions:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eFirst: What are the critical paths in your system? Not every code path. The ones that, if they break, create real incidents and wake people up. In e-commerce: order placement, payment processing, inventory updates. In SaaS: user authentication, data export, billing operations. In APIs: request validation, database queries, external service calls. You need to identify and understand these before you write a single log statement.\u003c/p\u003e\n\u003cp\u003eSecond: What can go wrong on each of these paths? Not everything theoretically possible. The specific failure modes you\u0026rsquo;ve actually seen in production or can reasonably expect based on your architecture. Payment timeout? Insufficient funds? Database deadlock? API rate limiting? Service unavailable? Malformed request? Rate limit exceeded? Each has a completely different diagnosis path and different fix. So you log for each of these specific scenarios, not for the thousands of things that don\u0026rsquo;t go wrong.\u003c/p\u003e\n\u003cp\u003eThird: What minimum information do I need to diagnose each specific failure? Not \u0026ldquo;all the data.\u0026rdquo; Not the entire request. The minimum information that tells you which specific failure mode occurred and \u003cem\u003ewhy\u003c/em\u003e. For a payment timeout, you need: order ID, amount, payment provider, timeout duration, retry count. You don\u0026rsquo;t need the entire customer object serialized. You don\u0026rsquo;t need the full response payload. You need the signal, not the noise.\u003c/p\u003e\n\u003cp\u003eThen—and only then—you instrument for exactly those scenarios. Not generically. Specifically and intentionally.\u003c/p\u003e\n\u003cp\u003eIn practice, this means source-generated log methods (using LoggerMessage) for each specific failure mode. Not generic \u0026ldquo;OrderProcessingStarted\u0026rdquo; and \u0026ldquo;OrderProcessingEnded\u0026rdquo; messages. Instead: \u0026ldquo;PaymentTimeout,\u0026rdquo; \u0026ldquo;PaymentDeclined,\u0026rdquo; \u0026ldquo;WarehouseQueueFull,\u0026rdquo; \u0026ldquo;InventoryInsufficient.\u0026rdquo; Each log message tells you exactly what state the system entered and what concrete cause triggered it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"anti-pattern-3-logging-without-correlation\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#anti-pattern-3-logging-without-correlation\" title=\"Anti-Pattern 3: Logging Without Correlation\"\u003eAnti-Pattern 3: Logging Without Correlation\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eA customer reports: \u0026ldquo;My order didn\u0026rsquo;t process.\u0026rdquo; In a microservices architecture, that single request touched four different services. Now you\u0026rsquo;re essentially a detective trying to solve a mystery.\u003c/p\u003e\n\u003cp\u003eWithout correlation IDs, finding the relevant logs across four different services becomes tedious, frustrating detective work. You search for \u0026ldquo;order timeout\u0026rdquo; and get 6 different orders from across the entire day. Which one is actually theirs? You cross-reference timestamps. You check payment logs. You check warehouse logs. You piece together a story. 30 minutes later, you finally find it. By then, the incident is already over. The customer has called your support team twice. You\u0026rsquo;re exhausted.\u003c/p\u003e\n\u003cp\u003eWith proper correlation, one single trace ID connects everything together. ASP.NET Core generates this automatically—it\u0026rsquo;s called HttpContext.TraceIdentifier. The same trace ID flows through every log entry for that specific request, across every service it touches. When a customer reports \u0026ldquo;my order didn\u0026rsquo;t process,\u0026rdquo; you search by that one trace ID and see every step: API received it, validation passed, payment service timed out, warehouse was never notified. Done. You understand the entire story in 30 seconds instead of 30 minutes.\u003c/p\u003e\n\u003cp\u003eThe W3C Trace Context standard makes this correlation work across service boundaries. It\u0026rsquo;s built into ASP.NET Core natively. You get it for free. But there\u0026rsquo;s a crucial requirement: you have to structure your logs so the trace ID is actually queryable—which means using structured logging (key-value pairs, not free-form text blobs).\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"anti-pattern-4-logging-performance-secrets\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#anti-pattern-4-logging-performance-secrets\" title=\"Anti-Pattern 4: Logging Performance Secrets\"\u003eAnti-Pattern 4: Logging Performance Secrets\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s a pattern I\u0026rsquo;ve seen derail production performance more often than most people admit: logging that hurts performance so severely that teams simply disable observability rather than pay the performance cost.\u003c/p\u003e\n\u003cp\u003eYour application runs beautifully on your local machine. You ship it to production. Suddenly in production, it feels sluggish. Latency starts climbing. P95 latency goes from 50ms to 200ms. Users complain. You add more logging to debug the slow path. Now it\u0026rsquo;s even slower. Much, much slower. You profile the application and find the surprising culprit: the logging itself is the bottleneck.\u003c/p\u003e\n\u003cp\u003eThis is the moment most teams give up on observability entirely. \u0026ldquo;It\u0026rsquo;s too expensive,\u0026rdquo; they say. What they really mean: \u0026ldquo;We instrumented it wrong and now we\u0026rsquo;re paying the performance price.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThe culprit: string formatting and object serialization happening automatically regardless of whether anyone is listening. You\u0026rsquo;re serializing objects, building strings, allocating temporary memory—all of it discarded if the log level isn\u0026rsquo;t even enabled. This is particularly insidious because it only hurts production performance (where logging is at higher levels) while looking perfectly fine in local testing (where you control the verbosity level).\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// KILLER: Always executes expensive work\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\"\u003eLogDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing user. FullDetails: {Details}\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\"\u003eJsonConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializeObject\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecomplexUser\u003c/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// BETTER: Guards it, but still wasteful\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\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEnabled\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=\"n\"\u003elogger\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 user. FullDetails: {Details}\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\"\u003eJsonConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializeObject\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecomplexUser\u003c/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// BEST: Source-generated logging—zero overhead when disabled\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 user. UserId={UserId}\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\"\u003eProcessingUser\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\"\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn production with Debug logging disabled, the first version \u003cem\u003estill executes the expensive serialization anyway\u003c/em\u003e. That\u0026rsquo;s performance death by a thousand cuts. The template parser runs. The object is serialized. The memory is allocated. Only \u003cem\u003ethen\u003c/em\u003e does the code check \u0026ldquo;is debug level enabled?\u0026rdquo; and discard the entire result. Wasted CPU cycles. Wasted memory. And this happens repeated thousands of times per second.\u003c/p\u003e\n\u003cp\u003eThis is exactly the kind of hidden performance killer that shows up and hurts production but not in load tests. Because load tests usually don\u0026rsquo;t add this kind of logging to their code paths.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThe Solution: Source-Generated Logging\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eSource-generated logging (LoggerMessage attribute, .NET 6+) completely flips this on its head. The compiler generates code at build time that knows: \u0026ldquo;this parameter matters, that one doesn\u0026rsquo;t. Here\u0026rsquo;s the most efficient way to capture and format it.\u0026rdquo; No runtime template parsing. No boxing. No wasted string allocation. Zero overhead when disabled.\u003c/p\u003e\n\u003cp\u003eA clarification: the performance gain is primarily noticeable in high-frequency logging scenarios (thousands of calls per second). For low-frequency events like error logging or rare business events, the difference is measurable but not dramatic. The real power of LoggerMessage is its consistency across high-volume paths. Also worth noting: LoggerMessage requires \u003ccode\u003epartial\u003c/code\u003e methods, which means you can\u0026rsquo;t use it everywhere—instance methods on regular classes need to be static partials, which limits where you can apply this pattern.\u003c/p\u003e\n\u003cp\u003eI wrote extensively about this pattern in my \u003ca href=\"/posts/compositeformat-performance-boost/\"\u003eCompositeFormat article\u003c/a\u003e, where I showed concretely how parsing overhead compounds at scale. The same principle applies here: parse once (at compile time), use a thousand times (at runtime). Source-generated logging is the logging equivalent of that core optimization. It delivers measurably better performance. It means measurably lower CPU usage. And the code is even cleaner and more maintainable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"anti-pattern-5-unstructured-logs-in-structured-systems\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#anti-pattern-5-unstructured-logs-in-structured-systems\" title=\"Anti-Pattern 5: Unstructured Logs in Structured Systems\"\u003eAnti-Pattern 5: Unstructured Logs in Structured Systems\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;ve set up Application Insights correctly. You\u0026rsquo;re sending structured logs to the cloud. But then someone does 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=\"c1\"\u003e// DON\u0026#39;T: Free-form text—not queryable or searchable\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=\"s\"\u003e$\u0026#34;Order 12345 failed. Payment service returned 429...\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// DO: Structured data—queryable and analyzable\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=\"s\"\u003e\u0026#34;Payment rate limited. OrderId={OrderId}, StatusCode={StatusCode}\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\"\u003estatusCode\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 second version is queryable. The first version is just noise that wastes storage.\u003c/p\u003e\n\u003cp\u003eApplication Insights, Datadog, Elasticsearch—all of these powerful tools only work effectively because logs are structured. When you log unstructured text, you throw away the tool\u0026rsquo;s entire value proposition. You might as well be writing to a flat file somewhere. You\u0026rsquo;ve spent significant money on enterprise observability and gained nothing from it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-practical-path-forward\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#the-practical-path-forward\" title=\"The Practical Path Forward\"\u003eThe Practical Path Forward\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eSo how do you actually fix these patterns? The answer isn\u0026rsquo;t more generic best practices. It\u0026rsquo;s not buying more tools. It\u0026rsquo;s building deliberate, intentional, carefully designed observability built specifically for your application.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-1-identify-your-critical-paths\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#step-1-identify-your-critical-paths\" title=\"Step 1: Identify Your Critical Paths\"\u003eStep 1: Identify Your Critical Paths\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWrite down the 3-5 user flows that actually matter in your system. Not every single code path. The ones where failure creates real incidents and angry customers.\u003c/p\u003e\n\u003cp\u003eFor an e-commerce system: order placement → payment processing → warehouse notification.\nFor a SaaS platform: user sign-up → authentication → data access → export.\nFor an API service: request validation → business logic → response serialization → client response.\u003c/p\u003e\n\u003cp\u003eYou\u0026rsquo;ll complete this exercise in an afternoon or two. It immediately clarifies what\u0026rsquo;s actually important in your system and what you should care about.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-2-map-failure-modes\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#step-2-map-failure-modes\" title=\"Step 2: Map Failure Modes\"\u003eStep 2: Map Failure Modes\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor each critical path, list concretely what can go wrong. Not everything theoretically possible. The specific failures you\u0026rsquo;ve actually dealt with in production:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePayment timeout (how long does it take to decide? What\u0026rsquo;s the timeout value?)\u003c/li\u003e\n\u003cli\u003eInsufficient funds (is this handled gracefully? Do you notify the user?)\u003c/li\u003e\n\u003cli\u003eService unavailable (do you have fallbacks? Do you retry?)\u003c/li\u003e\n\u003cli\u003eRate limiting (do you respect backoff headers? Do you queue?)\u003c/li\u003e\n\u003cli\u003eInvalid input (where\u0026rsquo;s the validation boundary? What gets validated?)\u003c/li\u003e\n\u003cli\u003eDatabase deadlock (how often does it happen? What query triggers it?)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis exercise takes longer than step one, but it\u0026rsquo;s where the real insight happens. You\u0026rsquo;re not speculating about what \u003cem\u003ecould\u003c/em\u003e theoretically go wrong. You\u0026rsquo;re building on what actually has gone wrong in production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-3-instrument-deliberately\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#step-3-instrument-deliberately\" title=\"Step 3: Instrument Deliberately\"\u003eStep 3: Instrument Deliberately\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNow you log only when something meaningful happens:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA critical path step completes (success or specific failure)\u003c/li\u003e\n\u003cli\u003eAn operation enters a retry/fallback state (you\u0026rsquo;re doing something non-standard)\u003c/li\u003e\n\u003cli\u003eA threshold is crossed (queue is full, latency exceeds SLA, rate limit triggered, circuit breaker opened)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNothing else. Not method entry/exit. Not variable assignments. Not successful intermediate steps that didn\u0026rsquo;t fail. Only things that directly help answer: \u0026ldquo;Why did this critical path fail?\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-4-make-logs-actionable\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#step-4-make-logs-actionable\" title=\"Step 4: Make Logs Actionable\"\u003eStep 4: Make Logs Actionable\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eHere\u0026rsquo;s the test: when someone reads a log line at 3 AM during an incident, can they immediately understand what was happening and what went wrong? Or do they need to cross-reference five other services, query the database, check five other log systems, and piece together a story?\u003c/p\u003e\n\u003cp\u003eIf it\u0026rsquo;s the latter, restructure your log. Make it self-contained. Include the context that matters. Make it so someone can understand what happened without detective work.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"step-5-use-sampling-for-scale\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#step-5-use-sampling-for-scale\" title=\"Step 5: Use Sampling for Scale\"\u003eStep 5: Use Sampling for Scale\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYou can\u0026rsquo;t keep every single log entry. But you actually don\u0026rsquo;t need to. Use context-aware, intelligent sampling:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKeep 100% of errors and warnings (these are rare and valuable)\u003c/li\u003e\n\u003cli\u003eFor information logs, consider adaptive sampling: sample heavily on errors (100%), moderately on warnings (50%), lightly on success paths (5-10%)\u003c/li\u003e\n\u003cli\u003eDisable debug logs in production entirely (add them on-demand when troubleshooting a specific incident)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eImportant note: Sampling must be consistent across all services in a distributed trace (W3C Trace Context propagates the \u003ccode\u003esampled\u003c/code\u003e flag for this reason). If one service samples at 10% and another at 50%, you\u0026rsquo;ll have incomplete and inconsistent traces. Either all services honor the same sampling decision, or you lose correlation.\u003c/p\u003e\n\u003cp\u003eWith this approach, you might sample 1 out of every 10 successful order completions. But you\u0026rsquo;ll still see 100 order completions per second even with sampling. You see the patterns. You see the anomalies. You catch bugs. And you\u0026rsquo;re not paying for 90% noise.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"real-example-the-safe-approach\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#real-example-the-safe-approach\" title=\"Real Example: The Safe Approach\"\u003eReal Example: The Safe Approach\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhen you combine all these principles—deliberate instrumentation, source-generated logging, correlation IDs, specific failure modes—the result looks like this:\u003c/p\u003e\n\u003cp\u003eYou log only when a critical path step completes. If it succeeds, one single log entry confirms it happened. If it fails, you log the specific failure mode (timeout, rate limit, validation error) with enough context to diagnose immediately. You use ActivitySource to track the operation through services. You keep the happy path silent—no noise about intermediate steps that didn\u0026rsquo;t fail.\u003c/p\u003e\n\u003cp\u003eInstead of sprawling code with dozens of unnecessary log statements, you have surgical, intentional instrumentation. Each log line earns its place because it answers a specific diagnostic question. You use W3C Trace Context headers (traceparent/tracestate) to correlate across services automatically. The result: when something breaks at 3 AM, you don\u0026rsquo;t sift through chaos. You have a clear narrative: here\u0026rsquo;s what the request tried to do, here\u0026rsquo;s where it failed in which service, here\u0026rsquo;s why. One single trace ID connects everything.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion-know-why-before-you-know-what\"\u003e\u003ca href=\"/posts/dotnet-advanced-logging/#conclusion-know-why-before-you-know-what\" title=\"Conclusion: Know Why Before You Know What\"\u003eConclusion: Know Why Before You Know What\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe difference between teams that own production and teams that merely survive it isn\u0026rsquo;t logging volume. It\u0026rsquo;s logging intelligence and intention.\u003c/p\u003e\n\u003cp\u003eThe teams with genuinely healthy observability don\u0026rsquo;t log more. They log smarter. They understand their failure modes deeply. They instrument not for completeness, but for purpose. They keep logs queryable because they know they\u0026rsquo;ll search them under pressure. They use sampling strategically instead of trying to keep everything.\u003c/p\u003e\n\u003cp\u003eMost importantly: they make every log line \u003cem\u003ecount\u003c/em\u003e. There\u0026rsquo;s no filler. No speculation. No \u0026ldquo;this might be useful someday.\u0026rdquo; Every log line answers a question.\u003c/p\u003e\n\u003cp\u003eMeanwhile, other teams are paying extra storage fees for logs nobody reads. They\u0026rsquo;re adding more logging and watching performance tank. They\u0026rsquo;re frustrated because diagnosis takes hours instead of minutes.\u003c/p\u003e\n\u003cp\u003eIt doesn\u0026rsquo;t have to be this way.\u003c/p\u003e\n\u003cp\u003eStart with the hardest question: \u0026ldquo;What would I need to see in a log line to immediately understand why this customer\u0026rsquo;s order failed? Why this API call timed out? Why this background job got stuck?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eThen instrument for exactly that. Nothing more. Nothing less.\u003c/p\u003e\n\u003cp\u003eWhen a bug escapes to production—and it will—you won\u0026rsquo;t be digging through gigabytes of noise hoping to find something relevant. You\u0026rsquo;ll have the signal right there in front of you. You\u0026rsquo;ll see what failed, why it failed, and what the system tried to do about it.\u003c/p\u003e\n\u003cp\u003eAt 3 AM, when production is burning and everyone is exhausted and frustrated, that\u0026rsquo;s the difference between \u0026ldquo;we found it in minutes and fixed it\u0026rdquo; and \u0026ldquo;we flew blind for hours and lost customers.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eBuild for that moment. Your future self will thank you.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-12-23T17:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-advanced-logging/","language":"en","summary":"Most .NET teams log 50MB per request and still can't diagnose the 3 AM outage. Fix the anti-patterns that turn observability into expensive noise.","tags":["observability","dotnet","csharp","architecture","bestpractices","cloudnative","performance"],"title":"Why Your Logging Strategy Fails in Production","url":"https://daily-devops.net/posts/dotnet-advanced-logging/"},{"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\u003e\u003cstrong\u003eHealthChecks 5.0 marks a decisive expansion: broader infrastructure coverage and cleaner mechanics with unapologetic .NET 10 readiness.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eFor most teams, health endpoints in 4.x were honestly just an afterthought. You know the drill: an abstract base class per check, copy‑pasted DI registrations, coverage shaped more by who shouted loudest than by actual risk. Capacity slipped before visibility caught up—streaming drains or index stalls were typically discovered by operators paging through dashboards rather than by proactive probes. Not ideal.\u003c/p\u003e\n\u003cp\u003e5.0 reverses that imbalance. It treats instrumentation coverage scope as a first‑order design goal—not some leftover chore. Instead of inheritance noise and manual wiring, you get generated clarity. Instead of gaps around vector stores, event hubs, graph traversals, or AI backends, you get deliberate surface area.\u003c/p\u003e\n\u003cp\u003eTwo parallel shifts (plus the platform tailwind) define the jump from 4.x to 5.0:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAggressive expansion of supported infrastructure domains\u003c/li\u003e\n\u003cli\u003ePerformance hardening via compile‑time code generation (eliminating inheritance boilerplate noise)\u003c/li\u003e\n\u003cli\u003eFormalized support for .NET 10\u003c/li\u003e\n\u003c/ol\u003e\n\u003ca href=\"https://github.com/dailydevops/healthchecks\" class=\"linked\" target=\"_blank\" rel=\"noopener external noreferrer\" title=\"Home of various health checks\"\u003e\n  \u003cimg src=\"/images/github-dailydevops-healthchecks.png\" class=\"repository\" width=\"1200\" height=\"630\" title=\"Home of various health checks\" alt=\"Home of various health checks\" /\u003e\n\u003c/a\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-fragmented-coverage-in-4x\"\u003e\u003ca href=\"/posts/healthchecks-5-0/#the-problem-fragmented-coverage-in-4x\" title=\"The Problem: Fragmented Coverage in 4.x\"\u003eThe Problem: Fragmented Coverage in 4.x\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYou could wire a handful of relational checks quickly; beyond that, friction mounted. Want Cassandra and Milvus side by side? That meant bespoke abstractions. Need early visibility into EventHubs partitions or Pub/Sub topics? Manual probes and dashboard spelunking.\u003c/p\u003e\n\u003cp\u003eGraph traversal sanity for Neo4j or JanusGraph? Usually deferred because \u0026ldquo;not this sprint.\u0026rdquo; AI integration (Ollama) lived outside uniform health semantics. The result: instrumented islands separated by latency swamps. Outages started cryptic (\u0026ldquo;search feels slow\u0026rdquo;) and matured into incidents only once saturation graphs finally caught up.\u003c/p\u003e\n\u003cp\u003eInstrumentation traditionally trails feature delivery—teams ship databases, streams, search clusters, and vector indexes faster than they wire consistent health diagnostics. That gap breeds ad‑hoc curl scripts, divergent endpoint semantics, and late-stage detection (usually when capacity is already bleeding).\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-solution-27-targeted-probes-with-unified-mechanics\"\u003e\u003ca href=\"/posts/healthchecks-5-0/#the-solution-27-targeted-probes-with-unified-mechanics\" title=\"The Solution: 27\u0026#43; Targeted Probes with Unified Mechanics\"\u003eThe Solution: 27+ Targeted Probes with Unified Mechanics\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e5.0 closes that gap with deliberate portfolio span. Here\u0026rsquo;s what expanded coverage looks like in practice:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMulti-cloud services\u003c/strong\u003e (AWS, Azure, GCP) unify under predictable semantics instead of bespoke wrappers. \u003cstrong\u003eHeterogeneous storage\u003c/strong\u003e—relational, columnar, time‑series, graph, vector—receives first-class, composable probes. \u003cstrong\u003eStreaming and event infra\u003c/strong\u003e (EventHubs, Pulsar, Pub/Sub) surface readiness before message backlogs cascade. And \u003cstrong\u003eAI pipelines\u003c/strong\u003e (Ollama models, embedding flows) are folded into standard operational baselines rather than treated as opaque, \u0026lsquo;best effort\u0026rsquo; adjuncts.\u003c/p\u003e\n\u003cp\u003eThe outcome? Fewer blind spots, coherent dashboards, faster mean-time-to-explanation.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"new-packages-vs-42061\"\u003e\u003ca href=\"/posts/healthchecks-5-0/#new-packages-vs-42061\" title=\"New Packages (vs 4.20.61)\"\u003eNew Packages (vs 4.20.61)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eUnified matrix for faster scanning; Area clarifies operational domain.\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\u003ePackage\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eArea\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePurpose\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\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.AWS.DynamoDB\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.AWS.DynamoDB\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / AWS NoSQL\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTable read/write probe \u0026amp; throughput sanity\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.AWS.EC2\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.AWS.EC2\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / AWS Compute\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eInstance reachability \u0026amp; state drift detection\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.EventHubs\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Azure.EventHubs\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / Azure Messaging\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNamespace accessibility + partition query\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Kusto\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Azure.Kusto\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / Azure Analytics\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eControl plane \u0026amp; lightweight query execution\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Search\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Azure.Search\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / Azure Search\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIndex availability \u0026amp; service status\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.GCP.Firestore\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.GCP.Firestore\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / GCP NoSQL\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDocument CRUD path liveness\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.GCP.PubSub\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.GCP.PubSub\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / GCP Messaging\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTopic existence \u0026amp; publish viability\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.GCP\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.GCP\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCloud / GCP Shared\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eShared primitives for unified GCP checks\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Cassandra\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Cassandra\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Columnar NoSQL\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSystem keyspace query \u0026amp; coordinator reachability\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.CockroachDb\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.CockroachDb\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Distributed SQL\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNode connectivity \u0026amp; lightweight SQL round‑trip\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Couchbase\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Couchbase\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / KV+Document\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBucket availability \u0026amp; KV latency\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.CouchDb\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.CouchDb\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Document\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEndpoint status \u0026amp; database listing touch\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.EventStoreDb\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.EventStoreDb\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Event Sourcing\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGossip / cluster info \u0026amp; stream probe\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.InfluxDB\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.InfluxDB\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Time-Series\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePing + test measurement write/read\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.JanusGraph\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.JanusGraph\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Graph\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTraversal sanity (simple vertex count)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.LiteDB\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.LiteDB\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Embedded NoSQL\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFile accessibility \u0026amp; collection probe\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.MariaDb\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.MariaDb\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Relational\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eConnection open + trivial query\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Milvus\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Milvus\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Vector\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCollection existence \u0026amp; vector insertion sanity\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.MySql.Devart\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.MySql.Devart\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Relational Driver\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDevart provider integration path check\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Neo4j\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Neo4j\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Graph\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBolt handshake + minimal cypher ping\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Npgsql.Devart\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Npgsql.Devart\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Relational Driver\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCross‑provider variant connectivity\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.OpenSearch\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.OpenSearch\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSearch / Distributed\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCluster health \u0026amp; index existence\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Oracle.Devart\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Oracle.Devart\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Relational Driver\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDevart Oracle session \u0026amp; probe query\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.SQLite.Devart\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.SQLite.Devart\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDatabase / Embedded Relational\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDevart SQLite file access \u0026amp; pragma ping\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Apache.Pulsar\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Apache.Pulsar\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMessaging / Streaming\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTenant lookup \u0026amp; topic metadata probe\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Consul\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Consul\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRegistry / Service Discovery\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCatalog read \u0026amp; KV key presence\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Ollama\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cem\u003e\u003cstrong\u003eNetEvolve.HealthChecks.Ollama\u003c/strong\u003e\u003c/em\u003e\u003c/a\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAI / LLM Local\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eModel list \u0026amp; lightweight prompt execution sanity\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=\"strategic-pivot\"\u003e\u003ca href=\"/posts/healthchecks-5-0/#strategic-pivot\" title=\"Strategic Pivot\"\u003eStrategic Pivot\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis release is a deliberate pivot from \u003cem\u003eabstract inheritance sprawl\u003c/em\u003e to \u003cem\u003edeterministic compilation\u003c/em\u003e. The direction harmonizes with the .NET 10 trajectory: trimming improvements, analyzer-driven contract enforcement. Explicit registries replace implicit conventions, tightening maintainability. Observability aligns—metrics map directly to known code paths instead of hidden lazy activation. And future readiness improves as static edges simplify evolution.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s an architectural stance: explicit beats implicit, generated beats hand‑wired repetition.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/healthchecks-5-0/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eVersion 5.0 broadens infrastructure coverage and simplifies the mechanics through source generation. The 27 new packages target domains that previously required manual workarounds—cloud services, graph databases, vector stores, streaming platforms, and local AI inference. The generator replaces repetitive inheritance patterns with explicit, deterministic registries.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re running multi-cloud stacks, heterogeneous storage, or expanding into vector and AI workloads, the expanded portfolio closes visibility gaps. The generator reduces boilerplate and tightens alignment between what\u0026rsquo;s configured and what\u0026rsquo;s deployed.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-20T23:00:00+01:00","id":"https://daily-devops.net/posts/healthchecks-5-0/","language":"en","summary":"HealthChecks 5.0 ships 27+ targeted AWS/Azure/GCP, graph, vector, streaming and AI probes and removes inheritance boilerplate via source generation.\n","tags":["netevolve","dotnet","csharp","performance","nuget"],"title":"NetEvolve.HealthChecks 5.0: 27+ Targeted Probes, Zero Boilerplate\n","url":"https://daily-devops.net/posts/healthchecks-5-0/"},{"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\u003e\u003cstrong\u003eMicrosoft wants you to believe .NET 10 is boring. They\u0026rsquo;re right — and that\u0026rsquo;s the best news we\u0026rsquo;ve had in years.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAfter the aggressive pace of .NET 6 through 9, Microsoft has shipped something different: a Long-Term Support release that doesn\u0026rsquo;t try to reinvent the platform. No experimental APIs. No architectural pivots. Just \u003cstrong\u003eruntime improvements, compiler optimizations, and tooling refinements\u003c/strong\u003e that production systems actually need.\u003c/p\u003e\n\u003cp\u003e.NET 10 extends support through \u003cstrong\u003eNovember 2028\u003c/strong\u003e — three full years of stability. For teams still recovering from the .NET 8 to .NET 9 migration cycle, that timeline feels like relief.\u003c/p\u003e\n\u003cp\u003eBut let\u0026rsquo;s be clear: this isn\u0026rsquo;t innovation theater. It\u0026rsquo;s \u003cstrong\u003eengineering maturity\u003c/strong\u003e. And if you\u0026rsquo;ve been chasing framework updates instead of shipping features, this LTS window is your chance to catch up.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-the-jit-compiler-finally-earned-its-keep\"\u003e\u003ca href=\"/posts/dotnet-10-released/#performance-the-jit-compiler-finally-earned-its-keep\" title=\"Performance: The JIT Compiler Finally Earned Its Keep\"\u003ePerformance: The JIT Compiler Finally Earned Its Keep\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s address the elephant in the room: \u003cstrong\u003e.NET has always promised performance\u003c/strong\u003e. Every release brings benchmarks showing 10-30% improvements. And every time, production systems see\u0026hellip; \u003cem\u003e5-7%\u003c/em\u003e if you\u0026rsquo;re lucky, meaningful only in tightly controlled scenarios.\u003c/p\u003e\n\u003cp\u003e.NET 10 changes that pattern, not through magic, but through \u003cstrong\u003esurgical optimizations\u003c/strong\u003e that compound across real workloads.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-actually-improved\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-actually-improved\" title=\"What Actually Improved\"\u003eWhat Actually Improved\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe JIT compiler now performs \u003cstrong\u003ephysical promotion\u003c/strong\u003e of struct members — meaning fewer memory indirections and tighter cache locality. It inlines array interface calls more aggressively and applies \u003cstrong\u003eadvanced loop vectorization\u003c/strong\u003e using AVX 10.2 instructions where supported.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTranslated:\u003c/strong\u003e your hot paths get faster without code changes.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s a minimal example showing the new \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e conversion improvements in C# 14:\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// C# 13: Manual conversion required\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eoldWay\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emyString\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/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// C# 14: Implicit conversion from string\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003enewWay\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emyString\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\u003eSubtle? Yes. But in tight loops processing text-heavy workloads, these small reductions in allocations add up.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-reality-check\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-reality-check\" title=\"The Reality Check\"\u003eThe Reality Check\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eDon\u0026rsquo;t expect miracles. If your API is slow because of database round-trips or inefficient queries, .NET 10 won\u0026rsquo;t fix that. But if you\u0026rsquo;re running compute-heavy services — data transformations, real-time analytics, batch processing — you\u0026rsquo;ll notice \u003cstrong\u003esmoother CPU usage and fewer GC pauses\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eWhen I migrated a few services from .NET 8 to .NET 9 last year, we measured around 7% throughput improvement on I/O-bound APIs and nearly 12% on CPU-intensive background workers. .NET 10 builds on that foundation with more predictable memory behavior and less GC jitter.\u003c/p\u003e\n\u003cp\u003eThe performance story here isn\u0026rsquo;t \u003cem\u003etwice as fast\u003c/em\u003e — it\u0026rsquo;s \u003cstrong\u003econsistently fast under load\u003c/strong\u003e. And in production, that consistency is worth more than benchmark theater.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"sdk-stability-where-net-9-stumbled-net-10-delivers\"\u003e\u003ca href=\"/posts/dotnet-10-released/#sdk-stability-where-net-9-stumbled-net-10-delivers\" title=\"SDK Stability: Where .NET 9 Stumbled, .NET 10 Delivers\"\u003eSDK Stability: Where .NET 9 Stumbled, .NET 10 Delivers\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s an uncomfortable truth: \u003cstrong\u003e.NET 9\u0026rsquo;s SDK had rough edges\u003c/strong\u003e. Workload resolution issues, inconsistent behavior between CI and local builds, and breaking changes in \u003ccode\u003edotnet publish\u003c/code\u003e that caught teams mid-sprint.\u003c/p\u003e\n\u003cp\u003eIf you migrated to .NET 9 early, you know what I\u0026rsquo;m talking about. We hit workload mismatch errors twice during our .NET 8 → .NET 9 migration — once in CI, once in our containerized deployments. The fix involved explicit \u003ccode\u003e--self-contained\u003c/code\u003e flags and careful SDK version pinning.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-net-10-fixed\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-net-10-fixed\" title=\"What .NET 10 Fixed\"\u003eWhat .NET 10 Fixed\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft addressed the fragility. The SDK now:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eResolves workloads deterministically\u003c/strong\u003e across environments\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFingerprints static assets automatically\u003c/strong\u003e, eliminating cache invalidation guesswork\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAligns container publishing\u003c/strong\u003e with the rest of the toolchain (no more surprise base image mismatches)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThat last point matters if you\u0026rsquo;re using \u003ccode\u003edotnet publish\u003c/code\u003e to generate container images directly. In .NET 9, it worked — until it didn\u0026rsquo;t, and then you spent an afternoon debugging why your Dockerfile suddenly produced different layers.\u003c/p\u003e\n\u003cp\u003e.NET 10 makes the build process \u003cstrong\u003eboring again\u003c/strong\u003e. And boring is what you want in CI/CD.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-hidden-win-roslyn-analyzers-play-nice\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-hidden-win-roslyn-analyzers-play-nice\" title=\"The Hidden Win: Roslyn Analyzers Play Nice\"\u003eThe Hidden Win: Roslyn Analyzers Play Nice\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cem\u003eOne overlooked improvement:\u003c/em\u003e Roslyn analyzers no longer slow down incremental builds as aggressively. If your project has 15+ analyzers enabled (you should), you\u0026rsquo;ll notice faster edit-compile-test cycles.\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s not revolutionary. But when you\u0026rsquo;re running that loop 50 times a day, the seconds add up.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"c-14-practical-improvements-not-syntax-experiments\"\u003e\u003ca href=\"/posts/dotnet-10-released/#c-14-practical-improvements-not-syntax-experiments\" title=\"C# 14: Practical Improvements, Not Syntax Experiments\"\u003eC# 14: Practical Improvements, Not Syntax Experiments\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eC# 14 ships with .NET 10, and the language team made a smart choice: \u003cstrong\u003eno experimental features\u003c/strong\u003e. Instead, they focused on filling gaps that developers work around daily.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"field-backed-properties\"\u003e\u003ca href=\"/posts/dotnet-10-released/#field-backed-properties\" title=\"Field-Backed Properties\"\u003eField-Backed Properties\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003ePreviously, auto-properties couldn\u0026rsquo;t expose their backing fields. Now they can:\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\"\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\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\"\u003eApiKey\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=\"c1\"\u003e// C# 14: Access the backing field directly\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\"\u003eClearSensitiveData\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003efield\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// \u0026#39;field\u0026#39; keyword references backing field\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eSmall change, but it eliminates the need for manual backing fields when you need direct access.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"linq-finally-gets-joins\"\u003e\u003ca href=\"/posts/dotnet-10-released/#linq-finally-gets-joins\" title=\"LINQ Finally Gets Joins\"\u003eLINQ Finally Gets Joins\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis one should\u0026rsquo;ve happened years ago. LINQ now supports \u003ccode\u003eLeftJoin()\u003c/code\u003e and \u003ccode\u003eRightJoin()\u003c/code\u003e without extension method hacks:\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\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomers\u003c/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\"\u003eLeftJoin\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\"\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\"\u003eId\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=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ec\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=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e,\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\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\"\u003eo\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// Customers without 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\"\u003eSelect\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\"\u003ec\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 you\u0026rsquo;ve written \u003ccode\u003eGroupJoin().SelectMany()\u003c/code\u003e gymnastics to fake left joins, you know why this matters.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"whats-still-missing\"\u003e\u003ca href=\"/posts/dotnet-10-released/#whats-still-missing\" title=\"What\u0026rsquo;s Still Missing\"\u003eWhat\u0026rsquo;s Still Missing\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNo async LINQ. No discriminated unions. No pipeline operators.\u003c/p\u003e\n\u003cp\u003eSome will call that conservative. I call it \u003cstrong\u003ediscipline\u003c/strong\u003e. C# 14 doesn\u0026rsquo;t rewrite the language — it sharpens the tools we already use.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migration-easier-than-net-9-but-not-trivial\"\u003e\u003ca href=\"/posts/dotnet-10-released/#migration-easier-than-net-9-but-not-trivial\" title=\"Migration: Easier Than .NET 9, But Not Trivial\"\u003eMigration: Easier Than .NET 9, But Not Trivial\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf you\u0026rsquo;re coming from .NET 8, the upgrade path is straightforward. If you\u0026rsquo;re on .NET 9, it\u0026rsquo;s almost invisible. But \u0026ldquo;almost\u0026rdquo; still requires validation.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-breaking-changes-youll-actually-hit\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-breaking-changes-youll-actually-hit\" title=\"The Breaking Changes You\u0026rsquo;ll Actually Hit\"\u003eThe Breaking Changes You\u0026rsquo;ll Actually Hit\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMicrosoft lists 47 breaking changes. Most won\u0026rsquo;t affect you. These will:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eASP.NET Core middleware order enforcement\u003c/strong\u003e — if you relied on loose ordering, expect build warnings (and potential runtime surprises).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEntity Framework Core query translation changes\u003c/strong\u003e — some LINQ queries that compiled in EF 8 now require client-side evaluation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJsonSerializer default behavior shifts\u003c/strong\u003e — particularly around null-handling and type discriminators in polymorphic scenarios.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eNone of these are blockers. But they will surface during integration testing if you skip unit coverage.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"what-we-learned-from-net-8--net-9\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-we-learned-from-net-8--net-9\" title=\"What We Learned from .NET 8 → .NET 9\"\u003eWhat We Learned from .NET 8 → .NET 9\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen we upgraded last year, we followed this pattern:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eRun your existing test suite first\u003c/strong\u003e — fix flaky tests before migrating. You don\u0026rsquo;t want to debug framework issues and test issues simultaneously.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUpgrade dependencies in isolation\u003c/strong\u003e — update NuGet packages one layer at a time (infrastructure, then domain, then API surface).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDeploy to a staging clone first\u003c/strong\u003e — not staging itself, but a true production clone with real load patterns.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe third step caught two issues we missed locally: a JSON serialization edge case and a gRPC deadline timeout that behaved differently under sustained load.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-upgrade-checklist\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-upgrade-checklist\" title=\"The Upgrade Checklist\"\u003eThe Upgrade Checklist\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBefore you start:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eConfirm all third-party libraries support .NET 10 (check NuGet compatibility)\u003c/li\u003e\n\u003cli\u003eUpdate your CI/CD pipeline SDK references\u003c/li\u003e\n\u003cli\u003eReview your \u003ccode\u003eglobal.json\u003c/code\u003e and lock the SDK version explicitly\u003c/li\u003e\n\u003cli\u003eValidate Docker base images if you\u0026rsquo;re containerized (\u003ccode\u003emcr.microsoft.com/dotnet/aspnet:10.0\u003c/code\u003e)\u003c/li\u003e\n\u003cli\u003eAudit custom Roslyn analyzers — some may not support C# 14 yet\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eRun your full test suite. Then run it again with diagnostics enabled. If you see warnings about obsolete APIs, address them now — they\u0026rsquo;ll become errors in .NET 11.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-to-migrate\"\u003e\u003ca href=\"/posts/dotnet-10-released/#when-to-migrate\" title=\"When to Migrate\"\u003eWhen to Migrate\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you\u0026rsquo;re on .NET 6 (LTS support ended \u003cstrong\u003eNovember 2024\u003c/strong\u003e), you\u0026rsquo;re already late. Move to .NET 10 directly.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re on .NET 8 (LTS ending \u003cstrong\u003eNovember 2026\u003c/strong\u003e), you have time — but the sooner you migrate, the longer you benefit from performance improvements in production.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re on .NET 9 (STS ending \u003cstrong\u003eNovember 2026\u003c/strong\u003e), migrate during your next sprints. Feel lucky, you might just find a hidden gem in the upgrade. The effort is minimal, and you gain three years of support instead of eighteen months.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-comes-next-the-platform-we-deserved-all-along\"\u003e\u003ca href=\"/posts/dotnet-10-released/#what-comes-next-the-platform-we-deserved-all-along\" title=\"What Comes Next: The Platform We Deserved All Along\"\u003eWhat Comes Next: The Platform We Deserved All Along\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET 10 represents something rare in software: \u003cstrong\u003ea mature platform that stopped chasing trends and started honoring its commitments\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThree years of LTS support means three years where your focus shifts from framework updates to product delivery. Where your CI pipelines stabilize instead of breaking every six months. Where runtime behavior becomes predictable enough that 3 AM production incidents become less frequent.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t the end of .NET\u0026rsquo;s evolution. It\u0026rsquo;s the foundation for what comes after.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-bigger-picture\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-bigger-picture\" title=\"The Bigger Picture\"\u003eThe Bigger Picture\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWith .NET 10 stable and locked in for the next three years, Microsoft can now take risks elsewhere — in Aspire, in Blazor United, in native AOT, in AI integrations — without destabilizing the core runtime. That separation between \u003cstrong\u003estable platform\u003c/strong\u003e and \u003cstrong\u003eexperimental tooling\u003c/strong\u003e is exactly what the ecosystem needs.\u003c/p\u003e\n\u003cp\u003eIf .NET 10 feels boring, it\u0026rsquo;s because boring is what production systems need. Excitement belongs in features, not in frameworks.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"the-opportunity\"\u003e\u003ca href=\"/posts/dotnet-10-released/#the-opportunity\" title=\"The Opportunity\"\u003eThe Opportunity\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eFor teams still on .NET Framework, this is your target for a rebuild and reconsider your strategy over the past few years. You have done something really really wrong, and have to pay the price of delayed modernization.\u003c/p\u003e\n\u003cp\u003eFor teams on .NET 6 or 8, this is your stabilization window. And for teams already on .NET 9, this is your chance to lock in the improvements without the upgrade treadmill.\u003c/p\u003e\n\u003cp\u003e.NET 10 won\u0026rsquo;t fix your architecture. It won\u0026rsquo;t eliminate your technical debt. It won\u0026rsquo;t make bad code good. But it will give you a runtime that \u003cstrong\u003eperforms predictably, builds consistently, and stays supported long enough to matter\u003c/strong\u003e. And in a world where frameworks change faster than products ship, that\u0026rsquo;s not just valuable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eThat\u0026rsquo;s exactly what we needed,\u003c/strong\u003e and I\u0026rsquo;m really looking forward to building on top of it for years to come.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-13T18:00:00+01:00","id":"https://daily-devops.net/posts/dotnet-10-released/","language":"en","summary":".NET 10 ships JIT physical promotion, AVX 10.2 loop vectorization, and C# 14 with LTS support through November 2028. Boring is finally the feature.\n","tags":["architecture","dotnet","csharp","codequality","microsoft","performance"],"title":".NET 10: Boring by Design, Reliable by Default\n","url":"https://daily-devops.net/posts/dotnet-10-released/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eThe \u003cstrong\u003e.NET ecosystem is changing faster than ever before\u003c/strong\u003e, and this time the shift runs deeper than a simple version number.\u003c/p\u003e\n\u003cp\u003eIn the last few months, I have seen a growing trend among organizations to delay their migration plans. \u003cstrong\u003eWe\u0026rsquo;ll wait for .NET 10 to stabilise.\u003c/strong\u003e - This sentiment is becoming increasingly common, without a clear understanding of what stability means in today\u0026rsquo;s accelerated software landscape.\u003c/p\u003e\n\u003cp\u003eOver the past years, Microsoft has unified runtimes, aligned frameworks, and compressed release cadences into a strict three-year Long-Term Support rhythm. Together with faster SDK iterations and an accelerating dependency landscape, these changes have quietly redefined what \u003cem\u003e\u003cstrong\u003estable\u003c/strong\u003e\u003c/em\u003e means in enterprise software.\u003c/p\u003e\n\u003cp\u003eThis evolution doesn\u0026rsquo;t create chaos—it creates compression.\nUpdate windows are shorter, dependencies are more interlinked, and security governance has become a continuous discipline rather than a periodic audit. As a result, timing itself is now a structural variable in the cost model of modern software.\u003c/p\u003e\n\u003cp\u003eFor almost a decade, organisations could afford to delay upgrades, waiting “one more release” in the name of caution. But those days are over. In the new ecosystem, every quarter of hesitation accumulates like interest on a loan. The debt isn’t in the code—it’s in the calendar. And that is precisely why targeting a \u003cstrong\u003e.NET 10 migration in Q1 2026\u003c/strong\u003e is not merely technically sensible, but economically strategic.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-5-whys-of-migration-timing\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#the-5-whys-of-migration-timing\" title=\"The 5 Whys of Migration Timing\"\u003eThe 5 Whys of Migration Timing\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"why-1--why-upgrade-at-all\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-1--why-upgrade-at-all\" title=\"Why 1 – Why upgrade at all?\"\u003e\u003cstrong\u003eWhy 1 – Why upgrade at all?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause remaining on older runtimes no longer preserves stability—it erodes it.\nThe three-year LTS rhythm means .NET 6 is out of support, and .NET 8 will follow in November 2026. Unsupported frameworks bring manual patching, fragmented libraries, and compliance exposure. What once felt like safety has become cost inertia.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-2--why-specifically-net-10\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-2--why-specifically-net-10\" title=\"Why 2 – Why specifically .NET 10?\"\u003e\u003cstrong\u003eWhy 2 – Why specifically .NET 10?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause .NET 10 completes the unification agenda Microsoft started years ago.\nFor the first time, runtime, SDK, and container models align seamlessly. Build systems behave predictably across platforms, dependency resolution has matured, and C# 14 integrates natively into DevOps toolchains. It’s the version where the ecosystem finally stabilises—and stability converts directly into lower operational overhead.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-3--why-now\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-3--why-now\" title=\"Why 3 – Why now?\"\u003e\u003cstrong\u003eWhy 3 – Why now?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause the ecosystem’s velocity has overtaken the enterprise pace.\nOpen-source maintainers, cloud vendors, and security standards evolve faster than corporate release plans. Two versions behind means you’re already managing exceptions instead of releases. Vulnerability patches and dependency updates increasingly assume modern SDKs, leaving older ones stranded. Waiting until 2027 simply means paying a premium for standing still.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-4--why-target-q1-2026\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-4--why-target-q1-2026\" title=\"Why 4 – Why target Q1 2026?\"\u003e\u003cstrong\u003eWhy 4 – Why target Q1 2026?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause that\u0026rsquo;s the moment when stability and ROI intersect.\nBy the first quarter after general availability, Microsoft\u0026rsquo;s initial cumulative updates are in place, partner libraries are aligned, and build tooling has settled.\nA Q1 2026 migration integrates naturally into fiscal planning, avoids year-end freezes, and delivers the full three-year LTS runway through late 2028.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-5--why-is-timing-an-economic-decision\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#why-5--why-is-timing-an-economic-decision\" title=\"Why 5 – Why is timing an economic decision?\"\u003e\u003cstrong\u003eWhy 5 – Why is timing an economic decision?\u003c/strong\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBecause time now governs cost curves.\nCloud workloads consume more compute under older runtimes—Microsoft\u0026rsquo;s own benchmarks show .NET 8 consuming 18-22% less memory than .NET 6 in containerised scenarios. Governance teams spend more cycles validating outdated dependencies; developers lose time adapting tooling instead of delivering value. Every delay drains budget and morale alike.\u003c/p\u003e\n\u003cp\u003eBut here\u0026rsquo;s the uncomfortable truth Microsoft won\u0026rsquo;t emphasise: the accelerated cadence benefits \u003cem\u003etheir\u003c/em\u003e cloud economics more directly than yours. Faster obsolescence drives Azure consumption of newer, optimised runtimes. Is that wrong? Not necessarily—but let\u0026rsquo;s not pretend the three-year LTS cycle was designed purely for developer convenience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-cost-of-waiting-dependency-and-developer-coupling\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#the-cost-of-waiting-dependency-and-developer-coupling\" title=\"The Cost of Waiting: Dependency and Developer Coupling\"\u003eThe Cost of Waiting: Dependency and Developer Coupling\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eConsider a financial-services platform still running on .NET 6.\nHalf its modules are maintained in-house, the rest by partner vendors and open-source projects.\nWhen a critical CVE appears in a transitive dependency—a telemetry or cryptography library, for instance—the internal teams can patch immediately. External vendors, however, must retest their modules and go through governance reviews. Open-source dependencies may require upstream fixes before new packages are even available.\u003c/p\u003e\n\u003cp\u003eThe result is version drift, duplicated effort, and expensive manual verification during audits.\nSecurity teams document exception after exception because not every library can be updated on command. Over a year, this coordination friction costs hundreds of engineer hours and more than \u003cstrong\u003e€200 000\u003c/strong\u003e in compliance overhead—without producing a single new feature.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s a real-world pattern I\u0026rsquo;ve seen repeatedly: teams add workarounds instead of addressing root causes.\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 6 workaround for incompatible dependency\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\"\u003eLegacyTelemetryAdapter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eOldTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/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\"\u003eLogEventAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eeventName\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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// Manual serialization because the library doesn\u0026#39;t support modern JSON APIs\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=\"n\"\u003eJsonConvert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSerializeObject\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=\"n\"\u003eEvent\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eeventName\u003c/span\u003e \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_client\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=\"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\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// Modern .NET 10 approach with updated dependency\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\"\u003eModernTelemetryAdapter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eITelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/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\"\u003eLogEventAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eeventName\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=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_client\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEventAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eeventName\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 adapter pattern above isn\u0026rsquo;t clever engineering—it\u0026rsquo;s technical debt accrued because upgrading the underlying telemetry library required upgrading the runtime first. Once the runtime is modern, the dependency can be modern, and the adapter disappears entirely.\u003c/p\u003e\n\u003cp\u003eMigrating to .NET 10 does not magically eliminate these dependencies—but it provides a unified, modern baseline where dependency visibility, communication, and automation can finally work together.\nOrganisations that succeed at this treat dependencies as part of their supply chain.\nThey \u003cstrong\u003ecommunicate proactively\u003c/strong\u003e with external maintainers, \u003cstrong\u003etrack dependency status\u003c/strong\u003e across internal and external repositories, and, where appropriate, \u003cstrong\u003econtribute back\u003c/strong\u003e—through pull requests, sponsorships, or shared testing infrastructure.\u003c/p\u003e\n\u003cp\u003eSupporting critical open-source projects is not altruism; it’s risk management.\nWhen your business depends on their libraries, your stability is their stability.\nA mature migration strategy therefore includes not only upgrading your code, but also strengthening the ecosystem you rely on.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"migration-as-strategic-sequencing\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#migration-as-strategic-sequencing\" title=\"Migration as Strategic Sequencing\"\u003eMigration as Strategic Sequencing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMethodologies like the “7 Rs” describe what kind of migration you perform—rehost, refactor, rebuild—but timing determines whether it delivers value.\nA successful .NET 10 transition sequences work around three axes:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eEconomic criticality\u003c/strong\u003e – modernise the workloads that generate or protect revenue first.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLifecycle synchronisation\u003c/strong\u003e – align runtime upgrades with dependency refreshes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCollaboration readiness\u003c/strong\u003e – ensure partners and open-source maintainers have the same timeline and resources.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eA \u003cstrong\u003eQ1 2026\u003c/strong\u003e target window achieves that balance: early enough to capture the efficiency and governance gains, late enough to benefit from ecosystem maturity.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"timing-as-a-financial-lever\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#timing-as-a-financial-lever\" title=\"Timing as a Financial Lever\"\u003eTiming as a Financial Lever\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003ethree-year LTS horizon\u003c/strong\u003e turns migration into a budget decision with measurable ROI.\nMove in Q1 2026 and enjoy full vendor support until late 2028.\nMove a year later and your amortisation window shortens to two years—an immediate 33 % reduction in return potential.\u003c/p\u003e\n\u003cp\u003eEarly .NET 10 preview benchmarks show promising efficiency gains: memory allocations down 15-20% in high-throughput APIs, container startup times improved by roughly 12%, and GC pause times reduced in server workloads. These aren\u0026rsquo;t marketing numbers—they\u0026rsquo;re patterns emerging from pre-release testing. Whether they hold in production across all workload types remains to be seen, but the direction is clear.\u003c/p\u003e\n\u003cp\u003eAcross container clusters and cloud-native deployments, these savings compound quickly.\nWhen timing and governance align, migration cost is recovered long before the next LTS arrives.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-economics-of-confidence\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#the-economics-of-confidence\" title=\"The Economics of Confidence\"\u003eThe Economics of Confidence\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOrganisations that manage timing as a discipline rather than a reaction consistently outperform peers in both cost control and security posture.\nThose that plan their migration now, test preview builds through late 2025, and execute in Q1 2026 achieve three enduring advantages:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePredictable stability\u003c/strong\u003e through 2028 under full vendor support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUnified dependency and security governance\u003c/strong\u003e, supported by transparent communication with external maintainers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStronger developer engagement\u003c/strong\u003e by investing in an ecosystem, not just a runtime.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWaiting until necessity forces change means continuing to pay the coordination tax: drifted dependencies, fragmented toolchains, and constant exception handling.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/timing-is-the-new-technical-debt/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe .NET ecosystem has matured; the economic model around it has changed.\nWhere upgrades once felt optional, they have become part of responsible cost management.\nMigrating to \u003cstrong\u003e.NET 10\u003c/strong\u003e is not a shortcut to perfection—it\u0026rsquo;s an entry ticket to a healthier, more predictable ecosystem.\u003c/p\u003e\n\u003cp\u003eTargeting completion in \u003cstrong\u003eQ1 2026\u003c/strong\u003e is not about speed; it\u0026rsquo;s about synchrony.\nThose who plan early, communicate clearly with dependency owners, and support the open-source projects they rely on will enjoy a three-year runway of stability and efficiency.\nThose who delay will discover that in software, as in finance, \u003cstrong\u003einterest compounds fastest on silence and inaction\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eI\u0026rsquo;ve watched too many teams postpone migrations \u003cem\u003ejust one more quarter\u003c/em\u003e—only to find themselves two versions behind, scrambling during a security incident, with vendors no longer prioritising their framework version. That scramble is expensive, stressful, and entirely avoidable.\u003c/p\u003e\n\u003cp\u003eIn this new era, the biggest risk isn\u0026rsquo;t outdated code—it\u0026rsquo;s unspoken dependencies and unplanned timing.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-11-12T18:00:00+01:00","id":"https://daily-devops.net/posts/timing-is-the-new-technical-debt/","language":"en","summary":"Why Q1 2026 .NET 10 migration is the most strategic move: proactive dependency management turns release-cycle timing from debt into advantage.\n","tags":["architecture","dotnet","csharp","performance","technicaldebt","bestpractices"],"title":".NET 10: Timing Is the New Technical Debt\n","url":"https://daily-devops.net/posts/timing-is-the-new-technical-debt/"},{"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\u003eIt begins like many stories in software: a well-intentioned developer joining a project, determined to do things properly. You arrive at a codebase that has grown organically, perhaps even chaotically. You decide you will bring order. You set up unit testing, you configure continuous integration, you measure code coverage. You write dozens or hundreds of tests. Every public method is touched, every branch is at least executed. The dashboard lights up green. You feel, quite frankly, on top of things.\u003c/p\u003e\n\u003cp\u003eThen one day, you discover a bug in production — a subtle logic error that wasn’t caught by any of your tests. The code that failed had a test. The test passed. The coverage tool declared that line covered. The build pipeline gave its all-clear. And yet, a customer faced an error and frustration ensued.\u003c/p\u003e\n\u003cp\u003eIn that moment you realize something simple: \u003cstrong\u003ecoverage only tells you that your code was executed, not that your tests are meaningful\u003c/strong\u003e. Your tests may run the code, but they may never actually verify its behavior, its intent or correctness. They claim safety, but they often deliver little more than comfort.\u003c/p\u003e\n\u003cp\u003eThis is precisely where Mutation Testing enters the story. It casts a harsh light on test suites that pass unquestioned, and forces them to prove their worth.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-mutation-testing-actually-does\"\u003e\u003ca href=\"/posts/tests-are-lying/#what-mutation-testing-actually-does\" title=\"What Mutation Testing Actually Does\"\u003eWhat Mutation Testing Actually Does\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eUnlike standard coverage analysis, Mutation Testing asks a deeper question: \u003cem\u003e\u0026ldquo;If this code were slightly wrong, would my tests notice?\u0026rdquo;\u003c/em\u003e In practice, a mutation-testing engine picks up your production code and introduces small, controlled modifications — called \u003cstrong\u003emutants\u003c/strong\u003e. For example, it might change a comparison operator (\u003ccode\u003e\u0026gt;=\u003c/code\u003e becomes \u003ccode\u003e\u0026gt;\u003c/code\u003e), invert a Boolean, replace a constant value, or alter a logical branch.\u003c/p\u003e\n\u003cp\u003eYour existing tests are then run against that mutated code. If a test fails, the mutation is considered \u003cstrong\u003ekilled\u003c/strong\u003e — your suite correctly caught the change. If a test still passes, the mutation \u003cstrong\u003esurvives\u003c/strong\u003e — meaning your tests failed to detect a behavioral change. The ratio of killed versus surviving mutants gives you a \u003cstrong\u003emutation score\u003c/strong\u003e, which is arguably a much more honest indicator of test quality than mere execution coverage.\u003c/p\u003e\n\u003cp\u003eThe virtue of this method is that it forces test suites to defend correctness rather than just confirm code paths. As the official Stryker.NET documentation puts it: \u003cem\u003ea mutant is a small change in your code … if the tests still pass, the mutant survived. If your tests are good they should catch the change and fail.\u003c/em\u003e\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"a-more-complex-example--real-world-business-logic-trap\"\u003e\u003ca href=\"/posts/tests-are-lying/#a-more-complex-example--real-world-business-logic-trap\" title=\"A More Complex Example — Real-World Business Logic Trap\"\u003eA More Complex Example — Real-World Business Logic Trap\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTo illustrate more fully, consider a slightly more elaborate example that might exist in an enterprise system. Suppose you have an employee pay-out logic in a service or domain layer.\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\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculatePayout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEmployee\u003c/span\u003e \u003cspan class=\"n\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsManager\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePerformanceRating\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e4\u003c/span\u003e\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBaseSalary\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e1.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\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsManager\u003c/span\u003e\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBaseSalary\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e1.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\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePerformanceRating\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e4\u003c/span\u003e\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBaseSalary\u003c/span\u003e \u003cspan class=\"p\"\u003e*\u003c/span\u003e \u003cspan class=\"m\"\u003e1.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\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\"\u003eemployee\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBaseSalary\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/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 first glance, this code appears straightforward. You write tests such 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=\"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\"\u003eManagerWithHighRatingGetsTopBonus\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEmployee\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsManager\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\"\u003ePerformanceRating\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eBaseSalary\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e5000\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\"\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\"\u003e6250\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculatePayout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\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}\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[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\"\u003eRegularEmployeeGetsNoBonus\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEmployee\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsManager\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\"\u003ePerformanceRating\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\"\u003eBaseSalary\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e4000\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\"\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\"\u003e4000\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculatePayout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\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}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBoth tests pass. You’re covered, right? The coverage tool shows nearly 100 % for this method. You feel confident.\u003c/p\u003e\n\u003cp\u003eThen a mutation testing run kicks in. Stryker mutates the code: it changes \u003ccode\u003e\u0026gt;= 4\u003c/code\u003e into \u003ccode\u003e\u0026gt; 4\u003c/code\u003e, or it alters the multiplier \u003ccode\u003e1.25m\u003c/code\u003e into \u003ccode\u003e1.10m\u003c/code\u003e, or perhaps it flips the order in which branches are evaluated. Your tests still pass. The mutation survives. That means your test suite did not notice the logic change. So your \u0026ldquo;complete coverage\u0026rdquo; was a mirage.\u003c/p\u003e\n\u003cp\u003eTo correct that you might need an additional test such 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=\"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\"\u003eManagerWithRatingExactlyAtBoundaryStillGetsTopBonus\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003ee\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eEmployee\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003eIsManager\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\"\u003ePerformanceRating\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=\"n\"\u003eBaseSalary\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e5000\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\"\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\"\u003e6250\u003c/span\u003e\u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eCalculatePayout\u003c/span\u003e\u003cspan class=\"p\"\u003e(\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}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWith that boundary test in place, the mutation turning \u003ccode\u003e\u0026gt;= 4\u003c/code\u003e into \u003ccode\u003e\u0026gt; 4\u003c/code\u003e would produce a test failure. This demonstrates how mutation testing forces you to think in terms of \u003cstrong\u003ebehavioral correctness\u003c/strong\u003e rather than simply in terms of \u0026ldquo;executing lines\u0026rdquo;.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"my-wake-up-call-with-strykernet\"\u003e\u003ca href=\"/posts/tests-are-lying/#my-wake-up-call-with-strykernet\" title=\"My Wake-Up Call with Stryker.NET\"\u003eMy Wake-Up Call with Stryker.NET\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet me share a personal story: I applied Stryker.NET to one of our flagship services. We had dozens of tests, coverage hovering at 95%+, and high confidence. I thought we were \u0026ldquo;done\u0026rdquo;.\u003c/p\u003e\n\u003cp\u003eWe ran Stryker. The results were sobering. We ran roughly \u003cem\u003e8,500 unit tests\u003c/em\u003e, a very large number of possible mutants. Out of all those tests, we had a survival rate of nearly 23% mutants. In other words, nearly one quarter of potential logical changes would go undetected by our tests.\u003c/p\u003e\n\u003cp\u003eIt felt like a punch in the gut. But it also felt like a gift. Because what followed was not shame but improvement. We began reviewing the surviving mutants, identifying which logic paths were untested or under-tested, and writing tests explicitly for them. Over subsequent runs the survival rate dropped, our mutation score improved, and our confidence increased — not because we chased a number, but because we improved our test suite’s behavior.\u003c/p\u003e\n\u003cp\u003eAt the end of this process, we found \u003cstrong\u003e12 undetected bugs\u003c/strong\u003e in our solution and a lot of additional edge cases that we hadn’t considered before. Every single minute we spent on this effort paid off in increased quality and reliability.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"strykernet-for-net--tooling-and-support\"\u003e\u003ca href=\"/posts/tests-are-lying/#strykernet-for-net--tooling-and-support\" title=\"Stryker.NET for .NET — Tooling and Support\"\u003eStryker.NET for .NET — Tooling and Support\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eStryker.NET is the de-facto propulsion engine for mutation testing in .NET. It supports .NET Core and .NET Framework projects, integrates with xUnit, NUnit, MSTest and TUnit, and is easy to install:\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 tool install -g dotnet-stryker\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn your test project directory you run:\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 stryker\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBy default it will mutate your code, run your suite repeatedly, and generate an HTML report in the \u003ccode\u003eStrykerOutput\u003c/code\u003e directory.\u003c/p\u003e\n\u003cp\u003eUnder the hood it uses the Roslyn syntax tree to identify code constructs and apply mutation operators (arithmetic, logical, string, etc.). The tool’s own documentation emphasises: \u0026ldquo;For most projects no configuration is needed. Simply run stryker and it will find your source project to mutate.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eStryker supports various mutation operator types: equivalent operator changes, arithmetic, logical, string replacements and more.\u003c/p\u003e\n\u003cp\u003eThe key point is: \u003cstrong\u003ethis tool tests the tests themselves.\u003c/strong\u003e\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"realistic-devops-integration--balancing-insight-with-cost\"\u003e\u003ca href=\"/posts/tests-are-lying/#realistic-devops-integration--balancing-insight-with-cost\" title=\"Realistic DevOps Integration — Balancing Insight with Cost\"\u003eRealistic DevOps Integration — Balancing Insight with Cost\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere is where many teams stumble: integrating mutation testing into your DevOps pipeline sensibly. Most articles might say \u0026ldquo;run it in CI on every pull request\u0026rdquo;, but the truth is more nuanced.\u003c/p\u003e\n\u003cp\u003eMutation testing is \u003cstrong\u003eresource-intensive\u003c/strong\u003e. It doesn’t execute your test suite once — it executes many times, with small code mutations each time. On a large codebase with thousands of tests, this means hours of build time, heavy CPU usage, and long delays. A paper on mutation testing at scale shows that sheer volume of mutants has been a barrier to adoption.\u003c/p\u003e\n\u003cp\u003eIn practice you want to adopt a measured approach. A workable pattern could be:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eSchedule Stryker.NET runs nightly or weekly when build agents are idle.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eTreat the mutation report as a diagnostic tool, not a blocking gate for every commit.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eStore HTML reports as build artifacts and share them with the team; review early in the next working day.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eUse incremental mutation testing for pull-requests:\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 stryker --since main\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis limits the scope of mutation to changed files and reduces runtime.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eDefine a trend-based metric rather than a rigid threshold: track mutation score over time rather than failing the build at 100%. Use, say, 75 % or 80 % as a warning boundary, not a hard stop.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eFocus mutation testing on critical modules — domain logic, validation rules, calculation services — rather than boilerplate, auto-generated code or trivial getters.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eI once attempted to run Stryker on every single pull request in our organization. The result was slow pipelines, frustrated engineers, and team pushes to bypass tests. We switched to a weekly schedule, freed up CI capacity, and made the reporting part of our Monday morning health check. The result: higher buy-in, better tests, and a steady drop in survived mutants.\u003c/p\u003e\n\u003cp\u003eIt is also important to communicate clearly that mutation testing is \u003cstrong\u003enot about speed\u003c/strong\u003e, but about \u003cstrong\u003equality insight\u003c/strong\u003e. Teams need to know that runs take time — sometimes hours, depending on repository size — and that the value lies in what you learn, rather than whether the build stays green quickly.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"managing-scope-complexity-and-equivalent-mutants\"\u003e\u003ca href=\"/posts/tests-are-lying/#managing-scope-complexity-and-equivalent-mutants\" title=\"Managing Scope, Complexity and Equivalent Mutants\"\u003eManaging Scope, Complexity and Equivalent Mutants\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eMutation testing brings its own practical complexities. Among them:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEquivalent mutants\u003c/strong\u003e: mutants that alter code but not behavior. They survive but don’t indicate a real deficiency. A recent empirical study found that correctly identifying equivalent mutants remains a challenge.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLarge mutant counts\u003c/strong\u003e: Without filtering, you may generate thousands of mutants. A paper on mutation testing at scale recommends incremental mutation and filtering.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePerformance tuning\u003c/strong\u003e: Stryker.NET offers options for parallel execution, mutation exclusion, and threshold configuration. Use these to keep runtime manageable.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTest suite quality prerequisite\u003c/strong\u003e: If you have almost no tests, mutation testing will bury you. It is most effective when you already have a reasonable baseline of tests. One blog notes: \u0026ldquo;if a team has difficulty finding time to write any tests at all, mutation testing is probably something that should take a backseat.\u0026rdquo;\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEven with these caveats, the benefit is clear: you find gaps you would not otherwise know existed, and you improve your test suite’s resilience.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-honest-metric\"\u003e\u003ca href=\"/posts/tests-are-lying/#the-honest-metric\" title=\"The Honest Metric\"\u003eThe Honest Metric\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIn the end, Mutation Testing offers an honest metric: it does not flatter you. It does not congratulate you for 97% coverage. It simply tells you how many logical changes your test suite would \u003cem\u003edetect\u003c/em\u003e. And often, that number is far lower than you expect.\u003c/p\u003e\n\u003cp\u003eStryker.NET brings that evaluation to the .NET ecosystem, supporting xUnit, NUnit, MSTest and TUnit. Whether you run it weekly, monthly or as part of a scheduled build, the insight remains meaningful.\u003c/p\u003e\n\u003cp\u003eIt forces you to shift your mindset: from simply running tests to \u003cstrong\u003edefending logic\u003c/strong\u003e, from coverage numbers to \u003cstrong\u003ebehavioral assurance\u003c/strong\u003e. Instead of asking \u0026ldquo;did my code run?\u0026rdquo; you begin to ask \u0026ldquo;if I changed the code, would my tests notice?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eAt the end of the day, green test suites are comfortable. Mutation-tested suites are trustworthy. And in a world where defects cost time, money and reputation, trust is what matters most.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-30T18:00:00+02:00","id":"https://daily-devops.net/posts/tests-are-lying/","language":"en","summary":"Stryker.NET exposes the blind spots line coverage hides—real lessons, richer examples, and a sustainable mutation testing flow for .NET DevOps.\n","tags":["csharp","dotnet","nuget","technicaldebt","testing"],"title":"Your Tests Are Lying — Mutation Testing in .NET","url":"https://daily-devops.net/posts/tests-are-lying/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLet’s be honest: health checks are the broccoli of .NET projects. Everyone says they have them, but nobody’s excited to eat their greens. What starts as a humble \u003ccode\u003eSELECT 1\u003c/code\u003e in a \u003ccode\u003etry/catch\u003c/code\u003e quickly explodes into a wild jungle of scripts, copy-pasted connection strings, and endpoints that only half the team remembers. Sure, it works—until it doesn’t. And when it breaks, it’s never at a good time.\u003c/p\u003e\n\u003cp\u003eWhen the inevitable outage hits, those scattered checks turn into static. Suddenly, your monitoring is just more noise, and ops are left chasing ghosts instead of fixing the real issue. That’s when it hits you: our health checks are stuck in the past, while the rest of our stack has moved on.\u003c/p\u003e\n\u003ca href=\"https://github.com/dailydevops/healthchecks\" class=\"linked\" target=\"_blank\" rel=\"noopener external noreferrer\" title=\"Various health checks based on `Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck`\"\u003e\n  \u003cimg src=\"/images/github-dailydevops-healthchecks.png\" class=\"repository\" width=\"1200\" height=\"630\" title=\"Various health checks based on `Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck`\" alt=\"Various health checks based on `Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck`\" /\u003e\n\u003c/a\u003e\n\u003cp\u003eThat’s where \u003cstrong\u003eNetEvolve.HealthChecks\u003c/strong\u003e storms in. Forget boring wrappers and boilerplate. This is a full-blown ecosystem—modular, lightning-fast, and built for the way you actually run software today. Configuration lives where it belongs: outside your code, right alongside the rest of your DevOps magic.\u003c/p\u003e\n\u003cp\u003eNo more copy-paste chaos. Define your checks once, give them names that make sense, and tweak their behavior on the fly. It’s clean, it’s safe, and—best of all—it grows with you. Your health checks finally become as agile as your infrastructure.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"why-configuration-first-because-change-happensfast\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#why-configuration-first-because-change-happensfast\" title=\"Why Configuration-First? Because Change Happens—Fast\"\u003eWhy Configuration-First? Because Change Happens—Fast\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet’s face it: code-driven health checks are a maintenance nightmare. Every environment tweak? Redeploy. Timeout needs a nudge? Open a PR, wait for review, redeploy again. New dependency? Hope you like hard-coded spaghetti. It’s a recipe for technical debt and late-night firefighting.\u003c/p\u003e\n\u003cp\u003eConfiguration-first flips the script. Developers set the \u003cem\u003ewhat\u003c/em\u003e, operators control the \u003cem\u003ehow\u003c/em\u003e. Change a timeout, swap a connection string, or update credentials—all without touching a line of code. JSON, environment variables, KeyVault: pick your weapon. Suddenly, health checks are flexible, auditable, and ready for anything your ops team throws at them.\u003c/p\u003e\n\u003cp\u003eThis isn’t just cleaner—it’s safer, faster, and way more fun. Health checks become a living part of your deployment, not a dusty afterthought.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"see-it-in-action\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#see-it-in-action\" title=\"See It in Action\"\u003eSee It in Action\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere’s how easy it gets. Want to check your SQL Server? Just say so:\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\"\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=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddSqlServer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;PrimaryDatabase\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\u003eNow, wire up the config:\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;HealthChecks\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;SqlServer\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;PrimaryDatabase\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;ConnectionString\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Server=tcp:sql1.contoso.local;Database=AppDb;User Id=app;Password=***;\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;Timeout\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e123\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003cem\u003eNo custom code. No redeploys for connection changes. No copy-paste across environments.\u003c/em\u003e\nThe name \u003ccode\u003ePrimaryDatabase\u003c/code\u003e ties it all together—code, config, logs, dashboards. It’s traceable, transparent, and totally under your control.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"options-of-course\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#options-of-course\" title=\"Options? Of Course\"\u003eOptions? Of Course\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNeed to set options in code for a quick test or dynamic setup? Go for 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=\"kt\"\u003evar\u003c/span\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=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddSqlServer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;PrimaryDatabase\u0026#34;\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\"\u003eConnectionString\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Server=tcp:sql1.contoso.local;Database=AppDb;User Id=app;Password=***;\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\"\u003eTimeout\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\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\u003eMix and match as you grow. Start simple, scale up, and never get boxed in by yesterday’s decisions. It couldn’t be easier, and it keeps your health checks as dynamic as your apps.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-makes-netevolvehealthchecks-a-game-changer\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#what-makes-netevolvehealthchecks-a-game-changer\" title=\"What Makes NetEvolve.HealthChecks a Game Changer?\"\u003eWhat Makes NetEvolve.HealthChecks a Game Changer?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost libraries pick a lane and stick to it. NetEvolve goes full autobahn. One design, one schema, one way to do things—across your whole stack. Relational, NoSQL, cloud, cache, messaging, search: it’s all here, all unified, all blazing fast.\u003c/p\u003e\n\u003cp\u003ePerformance? Absolutely. Benchmarks show NetEvolve outpaces the competition—lower allocations, snappier response times, and zero legacy baggage. When you’re running thousands of checks a day, that’s the difference between smooth sailing and a support ticket avalanche.\u003c/p\u003e\n\u003cp\u003eAnd because it’s built for modern .NET, you get the latest APIs, no crusty dependencies, and a foundation that’s ready for whatever comes next.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-not-aspnetcorehealthchecks\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#why-not-aspnetcorehealthchecks\" title=\"Why not AspNetCore.HealthChecks?\"\u003eWhy not \u003ca href=\"https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eAspNetCore.HealthChecks\u003c/code\u003e\u003c/a\u003e?\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eOn one hand, \u003ca href=\"https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eAspNetCore.HealthChecks\u003c/code\u003e\u003c/a\u003e offers a wide range of built-in checks and a dashboard for monitoring health status.\u003c/p\u003e\n\u003cp\u003eOn the other hand, each package feels like it has its own configuration style and approach, leading to inconsistencies and increased maintenance overhead. In contrast, \u003ca href=\"https://github.com/dailydevops/healthchecks\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eNetEvolve.HealthChecks\u003c/code\u003e\u003c/a\u003e offers a unified configuration-first approach that simplifies management and enhances scalability across diverse systems.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"plug-in-power-up\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#plug-in-power-up\" title=\"Plug In, Power Up\"\u003ePlug In, Power Up\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOnce you go declarative, integration is a breeze. Expose \u003ccode\u003e/health\u003c/code\u003e, \u003ccode\u003e/ready\u003c/code\u003e, or \u003ccode\u003e/live\u003c/code\u003e endpoints and let your orchestrator do the heavy lifting. Kubernetes, Azure App Service, load balancers—they all get it. Health-based routing, auto-recovery, zero drama.\u003c/p\u003e\n\u003cp\u003eTag your checks—\u003ccode\u003edatabase\u003c/code\u003e, \u003ccode\u003ecache\u003c/code\u003e, \u003ccode\u003eexternal\u003c/code\u003e, whatever. Decide what’s critical for readiness, what’s lightweight for liveness. Keep your uptime high and your alerts meaningful.\u003c/p\u003e\n\u003cp\u003eAnd don’t forget security. Health endpoints can spill secrets if you’re not careful. Lock them down to internal networks, authenticated users, or trusted agents. NetEvolve makes it easy to stay safe and visible at the same time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-big-picture-observability-evolved\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#the-big-picture-observability-evolved\" title=\"The Big Picture: Observability, Evolved\"\u003eThe Big Picture: Observability, Evolved\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNetEvolve.HealthChecks isn’t just a list of integrations—it’s a philosophy. Modern observability isn’t about writing more code. It’s about shrinking the gap between config, infrastructure, and real insight.\u003c/p\u003e\n\u003cp\u003eUnify your checks, give your teams a common language, and turn scattered logic into a versioned, configurable powerhouse. Collaboration gets easier, troubleshooting gets faster, and your monitoring finally keeps up with your ambitions.\u003c/p\u003e\n\u003cp\u003eThis is evolution with teeth—boilerplate out, clarity in. Your monitoring becomes a strategic asset, not a maintenance burden.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-current-netevolvehealthchecks-catalog\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#the-current-netevolvehealthchecks-catalog\" title=\"The current NetEvolve.HealthChecks Catalog\"\u003eThe current \u003ccode\u003eNetEvolve.HealthChecks\u003c/code\u003e Catalog\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNetEvolve.HealthChecks isn’t just a toolkit—it’s a huge set of libraries. Here’s what you get, grouped by what you need. Standardize your health monitoring across everything: databases, caches, queues, clouds, and more. One approach, zero confusion.\u003c/p\u003e\n\u003cdetails\u003e\n    \u003csummary\u003e\u003cstrong\u003eNetEvolve.HealthChecks Package Catalog / Expand for Details\u003c/strong\u003e\u003c/summary\u003e\n\u003ch3 id=\"core-and-abstractions\"\u003e\u003ca href=\"/#core-and-abstractions\" title=\"Core and Abstractions\"\u003eCore and Abstractions\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Abstractions/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Abstractions\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"relational-databases\"\u003e\u003ca href=\"/#relational-databases\" title=\"Relational Databases\"\u003eRelational Databases\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.SqlServer/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.SqlServer\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.SqlServer.Legacy/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.SqlServer.Legacy\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.SqlServer.Devart/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.SqlServer.Devart\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.MySql/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.MySql\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.MySql.Connector/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.MySql.Connector\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Npgsql/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Npgsql\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Sqlite/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Sqlite\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Sqlite.Legacy/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Sqlite.Legacy\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Oracle/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Oracle\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.DB2/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.DB2\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Odbc/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Odbc\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Firebird/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Firebird\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.DuckDB/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.DuckDB\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.ClickHouse/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.ClickHouse\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"nosql-and-document-databases\"\u003e\u003ca href=\"/#nosql-and-document-databases\" title=\"NoSQL and Document Databases\"\u003eNoSQL and Document Databases\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.MongoDb/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.MongoDb\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.RavenDb/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.RavenDb\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.ArangoDb/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.ArangoDb\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"caching\"\u003e\u003ca href=\"/#caching\" title=\"Caching\"\u003eCaching\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Redis/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Redis\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"messaging-and-streaming\"\u003e\u003ca href=\"/#messaging-and-streaming\" title=\"Messaging and Streaming\"\u003eMessaging and Streaming\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Apache.Kafka/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Apache.Kafka\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Redpanda/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Redpanda\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Apache.ActiveMq/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Apache.ActiveMq\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.RabbitMQ/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.RabbitMQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.ServiceBus/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure.ServiceBus\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"search-and-vector-databases\"\u003e\u003ca href=\"/#search-and-vector-databases\" title=\"Search and Vector Databases\"\u003eSearch and Vector Databases\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Elasticsearch/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Elasticsearch\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Qdrant/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Qdrant\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"azure-services\"\u003e\u003ca href=\"/#azure-services\" title=\"Azure Services\"\u003eAzure Services\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Blobs/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure.Blobs\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Queues/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure.Queues\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Tables/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure.Tables\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.ServiceBus/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure.ServiceBus\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.ApplicationInsights/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Azure.ApplicationInsights\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"aws-services\"\u003e\u003ca href=\"/#aws-services\" title=\"AWS Services\"\u003eAWS Services\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.AWS/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.AWS\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.AWS.S3/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.AWS.S3\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.AWS.SQS/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.AWS.SQS\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.AWS.SNS/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.AWS.SNS\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"identity-and-platform\"\u003e\u003ca href=\"/#identity-and-platform\" title=\"Identity and Platform\"\u003eIdentity and Platform\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Keycloak/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Keycloak\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Dapr/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Dapr\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"utilities-and-system\"\u003e\u003ca href=\"/#utilities-and-system\" title=\"Utilities and System\"\u003eUtilities and System\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.nuget.org/packages/NetEvolve.HealthChecks.Http/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eNetEvolve.HealthChecks.Http\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/details\u003e\n\n\n\n\n\u003ch2 id=\"ready-to-level-up-make-health-checks-effortless-bulletproof-and-actually-fun\"\u003e\u003ca href=\"/posts/netevolve-healthchecks/#ready-to-level-up-make-health-checks-effortless-bulletproof-and-actually-fun\" title=\"Ready to Level Up? Make Health Checks Effortless, Bulletproof, and Actually Fun\"\u003eReady to Level Up? Make Health Checks Effortless, Bulletproof, and Actually Fun\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet’s be real: health checks shouldn’t be a snooze. They should be rock-solid, invisible when things are good, and loud when it matters. NetEvolve.HealthChecks makes that a reality—no more friction, no more surprises, just pure reliability.\u003c/p\u003e\n\u003cp\u003eStill hand-coding checks? Hard-wiring secrets? Redeploying for every little tweak? It’s time to break the cycle. Let configuration do the heavy lifting and put your team back in the driver’s seat.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGet started now:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eVisit \u003ca href=\"https://github.com/dailydevops/healthchecks\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003egithub.com/dailydevops/healthchecks\u003c/a\u003e and grab what you need from NuGet. One command and you’re off:\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 NetEvolve.HealthChecks.SqlServer\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eReliability isn’t about more code—it’s about fewer surprises. With NetEvolve.HealthChecks, you get a future-proof, config-driven powerhouse that keeps your systems healthy and your team happy.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDon’t settle for boring. Make your health checks modern, effortless, and a little bit awesome.\u003c/strong\u003e\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-28T18:00:00+02:00","id":"https://daily-devops.net/posts/netevolve-healthchecks/","language":"en","summary":"Declarative, high-performance health checks for .NET driven by configuration—covers databases, queues, caches, and cloud services, not code.\n","tags":["csharp","dotnet","netevolve"],"title":"Configuration-First Health Checks for Modern .NET","url":"https://daily-devops.net/posts/netevolve-healthchecks/"},{"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":"\u003cp\u003eYour string operations are killing your API. You just haven\u0026rsquo;t measured it yet.\u003c/p\u003e\n\u003cp\u003eWhile you\u0026rsquo;re busy optimizing database queries and adding cache layers, thousands of string searches per second are quietly eating your CPU budget. The problem isn\u0026rsquo;t visible in your APM dashboard because it\u0026rsquo;s distributed across every request. But it\u0026rsquo;s there. Compounding. Scaling linearly with load.\u003c/p\u003e\n\u003cp\u003eI discovered this the hard way when a log processing API started choking under production traffic. The bottleneck? String validation and sanitization. The fix? A .NET 8 feature that delivered a \u003cstrong\u003e5x performance improvement\u003c/strong\u003e and let us shut down servers instead of adding them. And it\u0026rsquo;s gotten even better in .NET 9 and 10.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e isn\u0026rsquo;t a nice-to-have optimization. It\u0026rsquo;s the difference between infrastructure costs that scale with your success versus infrastructure costs that scale with your inefficiency.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-real-problem-string-operations-dont-scale-like-you-think\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#the-real-problem-string-operations-dont-scale-like-you-think\" title=\"The Real Problem: String Operations Don\u0026rsquo;t Scale Like You Think\"\u003eThe Real Problem: String Operations Don\u0026rsquo;t Scale Like You Think\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere\u0026rsquo;s what nobody tells you about string operations: they scale linearly with load. Double the traffic? Double the CPU usage. Add more features that parse strings? Multiply the pain.\u003c/p\u003e\n\u003cp\u003eTraditional approaches using \u003ccode\u003estring.IndexOfAny()\u003c/code\u003e or custom loops work fine when you\u0026rsquo;re processing dozens of requests per second. They fail silently when you\u0026rsquo;re processing thousands. No exceptions. No errors. Just slow, expensive CPU burn that compounds into massive infrastructure waste.\u003c/p\u003e\n\u003cp\u003eConsider a real scenario: a log aggregation service processing application logs in real-time. Each log entry needs sanitization, sensitive data detection, and validation before storage. That\u0026rsquo;s multiple string operations per log entry. Thousands of log entries per second. Millions of string operations per minute.\u003c/p\u003e\n\u003cp\u003eWith traditional methods, you\u0026rsquo;re leaving performance on the table. Modern CPUs have SIMD instructions that can process multiple characters simultaneously, but \u003ccode\u003eIndexOfAny()\u003c/code\u003e doesn\u0026rsquo;t use them consistently. It\u0026rsquo;s a generic solution for a problem that benefits from specialization.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s where \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e comes in. It\u0026rsquo;s a frozen, immutable set of values that .NET analyzes once at creation time, then optimizes specifically for your search pattern using the fastest algorithm available, whether that\u0026rsquo;s SIMD vectorization, bitmap lookups, or other strategies depending on your value set. The difference isn\u0026rsquo;t marginal. It\u0026rsquo;s transformational.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-makes-searchvaluest-different\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#what-makes-searchvaluest-different\" title=\"What Makes SearchValues\u0026lt;T\u0026gt; Different?\"\u003eWhat Makes \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e Different?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e was introduced in .NET 8. Most developers still haven\u0026rsquo;t heard of it. That\u0026rsquo;s a mistake that\u0026rsquo;s costing infrastructure budget.\u003c/p\u003e\n\u003cp\u003eWhen you create a \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e instance, .NET analyzes your value set \u003cstrong\u003eonce\u003c/strong\u003e and selects the most efficient search algorithm. SIMD vectorization when possible. Bitmap lookups for dense character sets. Optimized branching for sparse sets. The runtime chooses. You don\u0026rsquo;t.\u003c/p\u003e\n\u003cp\u003eYou pay the analysis cost once at startup. Then you reuse that optimized instance across millions of operations. That\u0026rsquo;s the trade-off: slightly more expensive creation, dramatically cheaper execution.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR; Pure performance win.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"net-9-and-net-10-getting-even-faster\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#net-9-and-net-10-getting-even-faster\" title=\".NET 9 and .NET 10: Getting Even Faster\"\u003e.NET 9 and .NET 10: Getting Even Faster\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e didn\u0026rsquo;t stop at .NET 8. Microsoft kept pushing performance further with each release, expanding both capabilities and raw speed.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"net-9-multi-substring-search\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#net-9-multi-substring-search\" title=\".NET 9: Multi-Substring Search\"\u003e.NET 9: Multi-Substring Search\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 8 gave us \u003ccode\u003eSearchValues\u0026lt;char\u0026gt;\u003c/code\u003e and \u003ccode\u003eSearchValues\u0026lt;byte\u0026gt;\u003c/code\u003e for character-level searches. .NET 9 expands this with \u003ccode\u003eSearchValues\u0026lt;string\u0026gt;\u003c/code\u003e for searching multiple substrings within a string.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBefore (Regex):\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// Looking for \u0026#34;error\u0026#34;, \u0026#34;warning\u0026#34;, or \u0026#34;critical\u0026#34; (case-insensitive)\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\"\u003eregex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eRegex\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;(?i)error|warning|critical\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eRegexOptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompiled\u003c/span\u003e\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\"\u003efound\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eregex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsMatch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogMessage\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\u003eAfter (SearchValues):\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\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eSearchValues\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\"\u003eLogKeywords\u003c/span\u003e \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\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e([\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;error\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;warning\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;critical\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"n\"\u003eStringComparison\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOrdinalIgnoreCase\u003c/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\"\u003efound\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogMessage\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=\"n\"\u003eContainsAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogKeywords\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 Regex compiler in .NET 9 \u003cstrong\u003euses this automatically\u003c/strong\u003e when it detects multi-substring patterns. Call it directly to skip regex parsing overhead entirely.\u003c/p\u003e\n\u003cp\u003eMulti-substring searches in log analysis became 3-4x faster in .NET 9. Patterns that were \u0026ldquo;too expensive\u0026rdquo; for every log entry suddenly became viable for real-time alerting.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR; .NET 9 is even faster for multi-substring searches.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch3 id=\"net-10-hardware-level-optimizations\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#net-10-hardware-level-optimizations\" title=\".NET 10: Hardware-Level Optimizations\"\u003e.NET 10: Hardware-Level Optimizations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e.NET 10 takes the optimization further by targeting CPU instruction sets directly. On AVX-512, it replaces two instructions with a single \u003ccode\u003ePermuteVar64x8x2\u003c/code\u003e, cutting CPU cycles in half. ARM64 gets cheaper \u003ccode\u003eUnzipEven\u003c/code\u003e instructions, delivering better performance on AWS Graviton and Azure Ampere instances. Case-insensitive searches benefit from extended fast-path logic that reduces validation overhead.\u003c/p\u003e\n\u003cp\u003eIf you\u0026rsquo;re running .NET 9 and seeing good results, .NET 10 makes the same operations \u003cstrong\u003e10-20% faster\u003c/strong\u003e without code changes. Just upgrade the runtime.\u003c/p\u003e\n\u003cp\u003eFor heavy text processing workloads, that 10-20% compounds across millions of operations. It\u0026rsquo;s the difference between needing an extra worker node versus staying within capacity.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR; Once again, .NET 10 is faster.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"production-numbers\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#production-numbers\" title=\"Production Numbers\"\u003eProduction Numbers\u003c/a\u003e\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBenchmarks lie. Production doesn\u0026rsquo;t.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn a real log processing pipeline handling production traffic, 10,000 log entries that previously took ~450ms now complete in ~85ms. That\u0026rsquo;s \u003cstrong\u003e5.3x faster on the same hardware\u003c/strong\u003e. Not \u003cem\u003e\u003cstrong\u003e20% faster\u003c/strong\u003e\u003c/em\u003e optimization theater. That\u0026rsquo;s handling five times more throughput without adding a single server. Infrastructure costs going down while traffic going up.\u003c/p\u003e\n\u003cp\u003eThis isn\u0026rsquo;t theoretical performance gains on a conference slide deck. This is the difference between scaling horizontally by adding servers versus scaling efficiently by using what you have. Between infrastructure costs that increase with growth versus costs that stay flat. Between firefighting capacity problems versus preventing them.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe business impact is direct: \u003cstrong\u003efewer servers, lower costs, same (or better) performance\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"log-sanitization-without-the-performance-tax\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#log-sanitization-without-the-performance-tax\" title=\"Log Sanitization Without the Performance Tax\"\u003eLog Sanitization Without the Performance Tax\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLogging should be cheap and invisible. Write to stdout, let your log shipper handle it, done. Reality is messier.\u003c/p\u003e\n\u003cp\u003eLog sanitization is expensive. Stripping sensitive data and control characters before storage becomes a bottleneck at scale. And every production system does it because compliance demands it. Regulations like GDPR, PCI-DSS, and HIPAA all require sanitizing logs.\u003c/p\u003e\n\u003cbr /\u003e\n\u003cp\u003eHere\u0026rsquo;s a simplified example of log sanitization that removes dangerous characters and redacts sensitive data 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=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\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\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eSystem.Buffers\u003c/span\u003e\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.Text\u003c/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\"\u003eLogSanitizer\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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// Characters that could indicate injection attempts or corrupt data\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\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eDangerousCharacters\u003c/span\u003e \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\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x0B\\x0C\\x0E\\x0F\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;\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B\\x1C\\x1D\\x1E\\x1F\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// Patterns that often precede sensitive data in logs\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\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eSensitiveMarkers\u003c/span\u003e \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\"\u003eSearchValues\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreate\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\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=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eSanitizeLogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAsSpan\u003c/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// Fast path: if no dangerous characters, return original\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\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContainsAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDangerousCharacters\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003eProcessSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elogEntry\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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// Slow path: rebuild without dangerous characters\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\u003cspan class=\"n\"\u003elogEntry\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        \u003cspan class=\"k\"\u003eforeach\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"k\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003eDangerousCharacters\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eContains\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ec\u003c/span\u003e\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\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"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\"\u003eProcessSensitiveData\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\"\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=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessSensitiveData\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\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=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Find patterns like \u0026#34;password=\u0026#34;, \u0026#34;token:\u0026#34;, \u0026#34;apiKey@\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eReadOnlySpan\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003espan\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\"\u003eAsSpan\u003c/span\u003e\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\"\u003emarkerIndex\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIndexOfAny\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eSensitiveMarkers\u003c/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\"\u003emarkerIndex\u003c/span\u003e \u003cspan class=\"p\"\u003e==\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=\"k\"\u003ereturn\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Redact the next 20 characters after sensitive markers\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\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringBuilder\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\"\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        \u003cspan class=\"n\"\u003eresult\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\"\u003espan\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSlice\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\"\u003emarkerIndex\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=\"n\"\u003eresult\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;[REDACTED]\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=\"n\"\u003eresult\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\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eThe Impact:\u003c/strong\u003e 87% reduction in sanitization time. Not in a benchmark. In production. Under real load.\u003c/p\u003e\n\u003cp\u003eThe fast path (clean logs) allocates \u003cstrong\u003ezero heap memory\u003c/strong\u003e. Garbage collector stays asleep. Sub-millisecond performance for 10KB log entries is the norm.\u003c/p\u003e\n\u003cp\u003eAcross millions of logs daily, that\u0026rsquo;s measurable infrastructure savings. We \u003cstrong\u003eshut down log processing workers\u003c/strong\u003e instead of adding them. Real money saved.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-you-need-to-know\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#what-you-need-to-know\" title=\"What You Need to Know\"\u003eWhat You Need to Know\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMicrosoft\u0026rsquo;s \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1870\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCA1870 analyzer\u003c/a\u003e throws warnings when it detects string searching that could use \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/api/system.buffers.searchvalues-1\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e\u003c/a\u003e. The warning appears on code like \u003ccode\u003etext.IndexOfAny(new[] { ',', ';', '|' })\u003c/code\u003e and suggests converting to \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e. The fix is straightforward: create a static readonly \u003ccode\u003eSearchValues\u0026lt;char\u0026gt; Delimiters = SearchValues.Create([',', ';', '|'])\u003c/code\u003e and use \u003ccode\u003etext.AsSpan().IndexOfAny(Delimiters)\u003c/code\u003e. The analyzer is helpful once you know when to act on it.\u003c/p\u003e\n\u003cp\u003eThe most critical mistake is creating instances in loops or hot paths. \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e is powerful but easy to misuse. The analysis work happens at creation time, which is expensive. The lookup cost is cheap. This means you need to store instances as static readonly fields. Pay the creation cost once at startup, then benefit from fast lookups across millions of operations. Creating \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e repeatedly destroys the performance advantage you\u0026rsquo;re trying to gain.\u003c/p\u003e\n\u003cp\u003ePremature optimization is still evil, but there\u0026rsquo;s a clear pattern where \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e delivers real value. The sweet spot is searching for \u003cstrong\u003e3+ different characters\u003c/strong\u003e or values in code that executes \u003cstrong\u003ehundreds to millions of times\u003c/strong\u003e, specifically in hot paths, per-request logic, or tight loops. Variable-length input where you can\u0026rsquo;t predict string size amplifies the benefits. Modern CPUs with SIMD support (most hardware from the last 5-7 years) show the biggest gains, often 5-10x faster. For searching 1-2 characters, just use \u003ccode\u003eIndexOf\u003c/code\u003e directly. It\u0026rsquo;s simpler and performs similarly. Skip \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e for code that runs once at startup, error handling paths that rarely execute, tiny fixed-length strings, or when you\u0026rsquo;re already bottlenecked on I/O operations. The overhead isn\u0026rsquo;t worth it for infrequent operations.\u003c/p\u003e\n\u003cp\u003eThe real test is production, not benchmarks. I\u0026rsquo;ve seen 2x to 10x improvements depending on workload, but your results will vary based on several factors. Character set size matters, where larger sets (10+ characters) benefit more from vectorization than small sets (2-3 characters). String length amplifies the speedup since more characters are processed. Search frequency compounds the benefits over time. CPU architecture plays a role too, with modern SIMD-capable processors showing 5-10x gains while older CPUs show 2-3x improvements. Don\u0026rsquo;t trust microbenchmarks that optimize for cache locality and predictable branching that production never has. Run load tests with production-like data. Measure wall-clock time and monitor allocations, not just throughput.\u003c/p\u003e\n\u003cp\u003ePerformance improvements in production aren\u0026rsquo;t synthetic benchmark gains. They\u0026rsquo;re real, measurable, budget-impacting improvements. Zero-allocation fast paths reduce GC pressure, which compounds in long-running services. Security validators that ran 3-5x faster meant user-facing latency reduction that customers noticed. ETL pipelines that completed 4x faster let us \u003cstrong\u003edecommission servers\u003c/strong\u003e instead of adding them. Not \u0026ldquo;up to\u0026rdquo; savings in a marketing slide. Actual, budgeted, we-turned-off-these-instances savings.\u003c/p\u003e\n\u003cp\u003eNot every use case benefits equally. Profile first, optimize second. Small character sets (2-3 characters) show minimal gains. Large character sets (10+ characters) deliver significant wins. Find your workload\u0026rsquo;s threshold. The creation cost is real, so single searches don\u0026rsquo;t benefit. This optimization is for repeated operations in high-frequency code paths.\u003c/p\u003e\n\u003cp\u003eIn high-throughput APIs and data pipelines, \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e delivered one of the highest ROI optimizations relative to effort. Minimal code changes. No architectural changes. No dependency updates or compatibility breaks. Measurable immediate gains. But ROI requires return. If you\u0026rsquo;re not processing thousands of operations per second, this won\u0026rsquo;t move the needle. Measure your use case and save energy for problems that actually matter.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-bottom-line\"\u003e\u003ca href=\"/posts/searchvalues-saved-us-from-scaling-hell/#the-bottom-line\" title=\"The Bottom Line\"\u003eThe Bottom Line\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e delivers measurable business value when applied correctly, but becomes noise when applied incorrectly. The difference lies in recognizing the pattern: high-frequency operations searching for multiple characters in variable-length input. This pattern shows up in log processing, input validation, CSV parsing, security filtering, and file path sanitization, essentially anywhere you\u0026rsquo;re repeatedly searching for multiple values in strings or spans where the size varies.\u003c/p\u003e\n\u003cp\u003eThe methodology is straightforward, though discipline matters. Start by measuring and profiling your hot paths to identify actual bottlenecks instead of guessing where problems might be. Apply the optimization selectively, focusing on repeated operations with 3+ characters in performance-critical code paths. Then validate the impact through benchmarking with realistic data under realistic load, not synthetic microbenchmarks that optimize for conditions production never sees. Treat CA1870 warnings as a starting point for investigation, not a mandate for action. Your profiler understands your workload better than any static analyzer ever could.\u003c/p\u003e\n\u003cp\u003eThe log sanitization example from earlier demonstrates the core pattern in action: high frequency, variable input, multiple search targets. This same combination appears across input validation, data parsing, security filtering, and content sanitization throughout production systems. When you find this pattern in your codebase, apply \u003ccode\u003eSearchValues\u0026lt;T\u0026gt;\u003c/code\u003e and measure what changes. When it works, it transforms performance in ways that show up directly in infrastructure costs. When it doesn\u0026rsquo;t deliver results, you\u0026rsquo;ve invested an hour learning something valuable about your workload\u0026rsquo;s actual characteristics.\u003c/p\u003e\n\u003cp\u003eThe impact is asymmetric by design. Your infrastructure budget notices the difference immediately through fewer servers, lower costs, and better resource utilization. Your customers notice nothing, which is exactly the point of effective performance optimization. Good performance work is invisible to users while being highly visible in cost structure and operational metrics. One less bottleneck when scaling. One fewer server to provision. One more problem solved before it escalates into a crisis that demands emergency intervention.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-20T17:30:00+01:00","id":"https://daily-devops.net/posts/searchvalues-saved-us-from-scaling-hell/","language":"en","summary":"How SearchValues in .NET 8-10 delivered 5x faster string operations, reduced infrastructure costs, and evolved with multi-substring optimization.","tags":["performance","bestpractices","codequality","csharp","dotnet","hidden-gems"],"title":"How SearchValues Saved Us From Scaling Hell","url":"https://daily-devops.net/posts/searchvalues-saved-us-from-scaling-hell/"},{"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\u003eMore than thirteen years ago, I was \u003cem\u003eforced\u003c/em\u003e by a project to switch from VB.NET to C# as my primary language. It wasn’t a change I had planned or wanted at the time. VB.NET had its quirks, but it also had a pragmatic and forgiving nature that made certain things feel\u0026hellip; effortless. Over the years, I grew to love C#: its precision, its tooling, its expressiveness. I never looked back.\u003c/p\u003e\n\u003cp\u003eWell – almost never.\u003c/p\u003e\n\u003cp\u003eThere was always one thing I missed. A tiny, elegant helper that VB.NET allowed me to write. A method that I’ve silently wished C# could express as cleanly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-vb\" data-lang=\"vb\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\u0026lt;HideModuleName\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ePublic\u003c/span\u003e \u003cspan class=\"k\"\u003eModule\u003c/span\u003e \u003cspan class=\"nn\"\u003eDisposableExtensions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026#39;\u0026#39;\u0026#39; \u0026lt;summary\u0026gt;Tries to release allocated resources.\u0026lt;/summary\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    \u0026lt;Extension, DebuggerStepThrough\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ePublic\u003c/span\u003e \u003cspan class=\"k\"\u003eFunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eTryDispose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eOf\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"ow\"\u003eAs\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003eClass\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIDisposable\u003c/span\u003e\u003cspan class=\"p\"\u003e})(\u003c/span\u003e\u003cspan class=\"k\"\u003eByRef\u003c/span\u003e \u003cspan class=\"n\"\u003esource\u003c/span\u003e \u003cspan class=\"ow\"\u003eAs\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"ow\"\u003eAs\u003c/span\u003e \u003cspan class=\"kt\"\u003eBoolean\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=\"n\"\u003esource\u003c/span\u003e \u003cspan class=\"ow\"\u003eIs\u003c/span\u003e \u003cspan class=\"k\"\u003eNothing\u003c/span\u003e \u003cspan class=\"k\"\u003eThen\u003c/span\u003e \u003cspan class=\"k\"\u003eReturn\u003c/span\u003e \u003cspan class=\"k\"\u003eTrue\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=\"n\"\u003esource\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=\"n\"\u003esource\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eNothing\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\"\u003eTrue\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=\"n\"\u003eex\u003c/span\u003e \u003cspan class=\"ow\"\u003eAs\u003c/span\u003e \u003cspan class=\"n\"\u003eException\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\"\u003eFalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eEnd\u003c/span\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=\"k\"\u003eEnd\u003c/span\u003e \u003cspan class=\"k\"\u003eFunction\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eEnd\u003c/span\u003e \u003cspan class=\"k\"\u003eModule\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis was a genuine life- and time-saver. It not only disposed of an object safely but also \u003cem\u003enulled out\u003c/em\u003e the reference afterwards. In VB.NET, this was possible because extension methods could take parameters \u003cem\u003e\u003ccode\u003eByRef\u003c/code\u003e\u003c/em\u003e – meaning the method could modify the original reference.\u003c/p\u003e\n\u003cp\u003eIn C#, for over a decade, this wasn’t possible. You could write an extension method to call \u003ccode\u003eDispose()\u003c/code\u003e, sure – but you couldn’t also set the caller’s variable to \u003ccode\u003enull\u003c/code\u003e. The language just didn’t allow it.\u003c/p\u003e\n\u003cp\u003eThat missing capability kept this one helper locked in the past, forever trapped in a VB.NET module.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"looking-ahead\"\u003e\u003ca href=\"/posts/still-waiting-for-the-final-piece/#looking-ahead\" title=\"Looking Ahead\"\u003eLooking Ahead\u003c/a\u003e\u003c/h2\u003e\n\n\n\n\n\u003ch3 id=\"net-10-and-c-14-extension-everything\"\u003e\u003ca href=\"/posts/still-waiting-for-the-final-piece/#net-10-and-c-14-extension-everything\" title=\".NET 10 and C# 14: Extension Everything\"\u003e.NET 10 and C# 14: Extension Everything\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eNow with \u003cstrong\u003e.NET 10\u003c/strong\u003e and \u003cstrong\u003eC# 14\u003c/strong\u003e, Microsoft introduces one of the most impactful additions to the language in years: \u003cstrong\u003eExtension Everything\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThis feature expands what extensions can do. Instead of being limited to methods, extensions can now define \u003cem\u003eproperties\u003c/em\u003e, \u003cem\u003eoperators\u003c/em\u003e, and even participate in pattern matching. The new syntax is expressive, elegant, and feels like the logical next step for C#.\u003c/p\u003e\n\u003cp\u003eHere’s what a modern \u003ccode\u003eTryDispose\u003c/code\u003e might look like using the new \u003ccode\u003eextension\u0026lt;T\u0026gt;\u003c/code\u003e construct:\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\"\u003enamespace\u003c/span\u003e \u003cspan class=\"nn\"\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\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\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eIDisposableExtensions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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\"\u003eextension\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\"\u003eT\u003c/span\u003e \u003cspan class=\"n\"\u003edisposable\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIDisposable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \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\"\u003eTryDispose\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \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\"\u003edisposable\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=\"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\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\"\u003edisposable\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=\"n\"\u003edisposable\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\"\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=\"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=\"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\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/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\u003eClean. Expressive. \u003cstrong\u003eImagine this:\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=\"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=\"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// Lots of code...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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\"\u003estream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTryDispose\u003c/span\u003e\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\"\u003eWarn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Could not release stream properly.\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\u003eIt reads perfectly. It’s elegant. And yet today, it’s still only half real. For now, the value won’t become \u003ccode\u003enull\u003c/code\u003e. The compiler still passes a copy. The concept is ready, but the implementation is not.\u003c/p\u003e\n\u003cp\u003eExactly what we’ve been waiting for.\u003c/p\u003e\n\u003cp\u003eExcept\u0026hellip; not quite.\u003c/p\u003e\n\u003cp\u003eWhile the syntax is here, the semantics aren’t – at least not yet. The extended instance (\u003ccode\u003edisposable\u003c/code\u003e) behaves as a copy of the reference, not a true alias to the caller’s variable. So even though the method compiles and runs, the original variable won’t be set to \u003ccode\u003enull\u003c/code\u003e afterwards. The dream of safely disposing and nulling in a single step remains just out of reach.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-it-still-matters\"\u003e\u003ca href=\"/posts/still-waiting-for-the-final-piece/#why-it-still-matters\" title=\"Why It Still Matters\"\u003eWhy It Still Matters\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThis syntax is more than just an aesthetic change—it represents an intentional step toward making the language more expressive and aligned with real-world usage. It points in the right direction, even if the final step hasn’t landed yet.\u003c/p\u003e\n\u003cp\u003eThe idea is simple: bring intent and safety together. Give developers the ability to describe what should happen to a resource without extra ceremony. Whether it’s cleanup, resetting, or transformation—such syntax makes intent explicit and code safer.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"closing-thoughts\"\u003e\u003ca href=\"/posts/still-waiting-for-the-final-piece/#closing-thoughts\" title=\"Closing Thoughts\"\u003eClosing Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis evolution in syntax feels like the final stretch in a journey that began more than a decade ago. It’s tantalizingly close—a feature that captures the spirit of what VB.NET once made easy, expressed in the elegance of modern C#.\u003c/p\u003e\n\u003cp\u003eVB.NET was always about readability and pragmatism. C# built on that foundation with structure and precision. With \u003cem\u003eExtension Everything\u003c/em\u003e, the two worlds almost meet—but not entirely. Not yet.\u003c/p\u003e\n\u003cp\u003eFor me, that’s both exciting and frustrating. The language can now express the shape of what I’ve wanted for years, but the behavior still isn’t there. So, I’ll keep that old VB.NET module around a little longer — partly out of nostalgia, partly because C# is still a step behind its little brother in this very specific trick. Even though VB.NET no longer holds any real relevance in my work, it’s amusing to see that it still has this one ace up its sleeve.\u003c/p\u003e\n\u003cp\u003eBut we’re getting close. Very close. \u003cstrong\u003eStill waiting for the final piece.\u003c/strong\u003e\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-10-06T09:00:00+02:00","id":"https://daily-devops.net/posts/still-waiting-for-the-final-piece/","language":"en","summary":"C# 14’s new 'Extension Everything' syntax comes close to VB.NET’s ByRef magic—but not quite. A witty look at what’s still missing in modern .NET.\n","tags":["csharp","dotnet","extensions","hidden-gems","vbnet","visualstudio"],"title":"Still Waiting for the Final Piece: C# 14 Comes Close","url":"https://daily-devops.net/posts/still-waiting-for-the-final-piece/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e.NET 10 RC 1 is knocking at the door, marking the first release candidate and offering the .NET community a detailed preview of what’s to come in the next LTS cycle. While not the final release, RC 1 is \u0026ldquo;go-live\u0026rdquo; supported and represents the feature-complete platform that will soon become .NET 10 LTS. In this article, I’ll try to give a rough overview of the architectural impact of .NET 10 RC 1, focusing on the latest C# 14 features, under-the-hood performance improvements, and strategic considerations for the upcoming LTS.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"net10-rc1-at-a-glance-lts-on-the-horizon-key-improvements-now\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#net10-rc1-at-a-glance-lts-on-the-horizon-key-improvements-now\" title=\".NET 10 RC 1 at a Glance: LTS on the Horizon, Key Improvements Now\"\u003e.NET 10 RC 1 at a Glance: LTS on the Horizon, Key Improvements Now\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWith .NET 10 RC 1, Microsoft is inviting early adopters to prepare for the next major LTS release (expected with the final GA later this year). RC 1 brings broad improvements across the platform:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRuntime Enhancements:\u003c/strong\u003e Smarter JIT compilation (improved inlining, devirtualization, and loop optimizations) and Native AOT advances result in faster code execution. Memory management sees gains with better stack allocation and garbage collection optimizations, including new write barriers on ARM64 for reduced pause times.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSDK and Tooling:\u003c/strong\u003e RC 1 introduces the ability for console apps to publish directly to container images, new SDK options for controlling image formats, and CLI improvements like standardized command ordering and tab-completion generation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eASP.NET Core \u0026amp; Libraries:\u003c/strong\u003e ASP.NET Core 10.0 (RC 1) updates include Blazor WebAssembly loading improvements, enhanced minimal APIs, and OpenAPI 3.1 support. Core libraries gain new APIs and performance tweaks—like stricter and more efficient JSON serialization.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe key theme of RC 1: improve performance and developer productivity while ensuring a stable path to LTS for production environments.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"c14-new-language-features-in-net10-rc1\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#c14-new-language-features-in-net10-rc1\" title=\"C# 14: New Language Features in .NET 10 RC 1\"\u003eC# 14: New Language Features in .NET 10 RC 1\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eC# 14 ships with .NET 10 RC 1, delivering language enhancements that simplify code and enable new patterns:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eField-Backed Properties:\u003c/strong\u003e Easily reference the compiler-generated backing field inside a property accessor using the new \u003ccode\u003efield\u003c/code\u003e keyword, streamlining property evolution.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003enameof\u003c/code\u003e on Generic Types:\u003c/strong\u003e Supports unbound generics (e.g., \u003ccode\u003enameof(List\u0026lt;\u0026gt;)\u003c/code\u003e), handy for logging and diagnostics.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eImplicit \u003ccode\u003eSpan\u0026lt;T\u0026gt;\u003c/code\u003e Conversions:\u003c/strong\u003e More natural and efficient use of spans for high-performance code.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLambda Parameter Modifiers:\u003c/strong\u003e Use \u003ccode\u003eref\u003c/code\u003e, \u003ccode\u003ein\u003c/code\u003e, \u003ccode\u003eout\u003c/code\u003e, or \u003ccode\u003escoped\u003c/code\u003e modifiers in lambda expressions without explicit types, increasing flexibility for advanced scenarios.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePartial Constructors and Events:\u003c/strong\u003e Declare constructors and events as \u003ccode\u003epartial\u003c/code\u003e, improving code organization in large or generated projects.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExtension Members via \u003ccode\u003eextension\u003c/code\u003e Blocks:\u003c/strong\u003e Group extension methods and properties, even static ones, in a natural way.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNull-Conditional Assignment:\u003c/strong\u003e Assign values using the null-conditional operator on the left side, reducing boilerplate.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUser-Defined Compound Operators:\u003c/strong\u003e Explicitly define \u003ccode\u003e+=\u003c/code\u003e, \u003ccode\u003e++\u003c/code\u003e, etc., for custom types, offering more control to library authors.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAll these features are available in .NET 10 RC 1 and will be fully supported at GA.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-gains-in-net10-rc1\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#performance-gains-in-net10-rc1\" title=\"Performance Gains in .NET 10 RC 1\"\u003ePerformance Gains in .NET 10 RC 1\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe performance enhancements in .NET 10 RC 1 are substantial:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSmarter JIT Compiler:\u003c/strong\u003e RC 1 improves codegen, method inlining, and virtual call elimination—especially in array and \u003ccode\u003eIEnumerable\u0026lt;T\u0026gt;\u003c/code\u003e usage.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReduced Allocations:\u003c/strong\u003e More aggressive stack allocation for small arrays and transient objects means fewer GC pauses.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNativeAOT Improvements:\u003c/strong\u003e RC 1 expands AOT support for various patterns, resulting in even faster startup and smaller binaries.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHardware Intrinsics:\u003c/strong\u003e Ready for AVX10.2, preparing for next-gen hardware.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGC Optimizations:\u003c/strong\u003e More efficient write barriers for Arm64, reducing GC pause times on modern cloud servers.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThese changes are available today in RC 1, and will deliver even more value as you roll forward to .NET 10 GA.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"impact-on-existing-applications-and-breaking-changes\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#impact-on-existing-applications-and-breaking-changes\" title=\"Impact on Existing Applications and Breaking Changes\"\u003eImpact on Existing Applications and Breaking Changes\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAdopting .NET 10 RC 1 is generally a smooth process for modern .NET applications (especially those already on .NET 6, 7, or 8), but every major release introduces a set of important changes—both \u003cstrong\u003ebreaking changes\u003c/strong\u003e and subtle behavioral differences. Understanding these up front ensures that architectural decisions and upgrade paths are properly managed.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"obsolete-and-deprecated-apis\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#obsolete-and-deprecated-apis\" title=\"Obsolete and Deprecated APIs\"\u003eObsolete and Deprecated APIs\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eASP.NET Core\u003c/strong\u003e: Several APIs and features are now marked as obsolete or removed. For example, \u003ccode\u003eIActionContextAccessor\u003c/code\u003e is deprecated in ASP.NET Core 10. If your solution relied on global access to \u003ccode\u003eActionContext\u003c/code\u003e, you should switch to more explicit DI patterns. Similarly, runtime Razor view compilation is no longer supported in the same way; dynamic compilation at runtime is now discouraged for performance and security reasons. Refactor views to use pre-compilation and follow the new guidance for dynamic scenarios.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLegacy Behaviors Cleaned Up\u003c/strong\u003e: Some behaviors that were previously tolerated for backward compatibility are now stricter or have been removed altogether. For instance, certain legacy configuration providers or authentication flows in ASP.NET Core may no longer behave as before.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"changes-in-core-libraries\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#changes-in-core-libraries\" title=\"Changes in Core Libraries\"\u003eChanges in Core Libraries\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNon-nullable Properties\u003c/strong\u003e: Properties such as \u003ccode\u003eFilePatternMatch.Stem\u003c/code\u003e (in file globbing APIs) are now non-nullable. If your code previously checked for \u003ccode\u003enull\u003c/code\u003e or allowed for missing stems, you’ll need to adjust for the new contract, or you may encounter new compiler warnings or exceptions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAsync LINQ in BCL\u003c/strong\u003e: The core libraries now include \u003ccode\u003eSystem.Linq.AsyncEnumerable\u003c/code\u003e. If you previously used the community-provided async LINQ NuGet package, you may face type conflicts or ambiguous references. Removing the extra package and using the in-box types resolves this issue.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConfiguration Binding Behavior\u003c/strong\u003e: Configuration binding now \u003cstrong\u003epreserves \u003ccode\u003enull\u003c/code\u003e values\u003c/strong\u003e by default. Where earlier versions might have ignored explicit \u003ccode\u003enull\u003c/code\u003es in config sources, RC 1 will bind them. This can surface bugs or require logic changes in options classes or startup code.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"platform-and-environment-changes\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#platform-and-environment-changes\" title=\"Platform and Environment Changes\"\u003ePlatform and Environment Changes\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eContainer Images Default to Ubuntu\u003c/strong\u003e: The official .NET 10 RC 1 container images now use Ubuntu 22.04 as the base image by default. Previously, you may have been relying on Debian or Alpine-based images. This change affects container size, security baselines, and possibly compatibility with tools or scripts expecting a different OS environment. If your deployment process or compliance requirements depend on a specific distro, update your Dockerfiles to use the explicit tag (e.g., \u003ccode\u003ealpine\u003c/code\u003e or \u003ccode\u003edebian\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDisabled HTTP/3 by Default with Trimming\u003c/strong\u003e: When using trimming (for self-contained, minimal-size deployments), HTTP/3 support is now disabled unless explicitly opted-in. This prevents accidental inclusion of large, unnecessary networking code in trimmed apps but can break scenarios relying on HTTP/3, such as modern gRPC or advanced ASP.NET Core configurations.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLogging Changes\u003c/strong\u003e: Console logging is now smarter—by default, it no longer duplicates messages across multiple providers/outputs. If you had custom logging logic or were expecting certain log flows, validate that your log output remains as intended after the upgrade.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"nuget-and-dependency-management\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#nuget-and-dependency-management\" title=\"NuGet and Dependency Management\"\u003eNuGet and Dependency Management\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePackage Compatibility\u003c/strong\u003e: Most NuGet packages targeting .NET Standard 2.0+ or .NET 6+ will work with .NET 10 RC 1. However, any packages using reflection or internal runtime APIs might require updates. Check for new versions of critical dependencies, and validate all third-party libraries in your build and test pipelines.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFramework Targeting\u003c/strong\u003e: .NET 10 RC 1 is stricter about target framework compatibility. Projects that reference legacy assemblies or unsupported TFMs will generate more explicit build warnings or errors, driving better practices but possibly requiring codebase updates.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"security-and-compliance\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#security-and-compliance\" title=\"Security and Compliance\"\u003eSecurity and Compliance\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCryptography Baseline Updates\u003c/strong\u003e: .NET 10 RC 1 updates cryptographic algorithms, strengthens TLS defaults, and adds support for new standards (including post-quantum algorithms and improved AES support). Applications with custom cryptography, or those that interoperate with older protocols, should be tested carefully to ensure continued compatibility and compliance.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"windows-desktop-and-other-subsystems\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#windows-desktop-and-other-subsystems\" title=\"Windows Desktop and Other Subsystems\"\u003eWindows Desktop and Other Subsystems\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eWinForms/WPF Updates\u003c/strong\u003e: There are numerous small but impactful updates in Windows desktop stacks (WinForms, WPF), including new APIs, DPI scaling improvements, and better accessibility. Some legacy controls or behaviors may be removed or altered. Review the official breaking changes for desktop-specific notes if you target Windows.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"practical-recommendations\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#practical-recommendations\" title=\"Practical Recommendations\"\u003ePractical Recommendations\u003c/a\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReview the \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/core/compatibility/10.0\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eofficial breaking changes documentation\u003c/a\u003e\u003c/strong\u003e and filter for all frameworks your solutions depend on (ASP.NET Core, Entity Framework Core, WinForms, WPF, etc.).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTest Early and Often:\u003c/strong\u003e Use the RC 1 bits in development or staging to surface behavioral changes. Pay special attention to integration boundaries—especially serialization, networking, and interop code.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAutomate Compatibility Checks:\u003c/strong\u003e Update your CI pipelines to include warnings-as-errors and run tests with .NET 10 RC 1 to proactively identify issues.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBy systematically addressing these changes, you ensure a smooth transition to .NET 10 LTS and avoid common pitfalls as the ecosystem moves forward.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"architectural-considerations-for-net10-rc1\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#architectural-considerations-for-net10-rc1\" title=\"Architectural Considerations for .NET 10 RC 1\"\u003eArchitectural Considerations for .NET 10 RC 1\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWith .NET 10 RC 1, architectural planning gains new momentum for modern cloud-native and high-performance development. The direct support for containerization enables streamlined deployment pipelines, allowing teams to produce ready-to-run images straight from their build processes and reducing operational friction in multi-service environments. The expanded capabilities of NativeAOT open new scenarios for applications demanding rapid startup and minimal resource consumption, such as serverless workloads, lightweight microservices, or command-line tools. Performance improvements across the runtime—particularly in JIT optimizations and memory allocation—encourage a more declarative and idiomatic use of C# constructs, enabling cleaner architectures without sacrificing efficiency. Security also receives a boost, with updated cryptography baselines and compliance with emerging standards, which is increasingly relevant for regulated industries and zero-trust environments. At the same time, C# 14’s new features, such as extension members and partial constructors, offer architects and teams greater flexibility in organizing large codebases and adopting modern software design patterns. Altogether, .NET 10 RC 1 provides a compelling foundation for scalable, maintainable, and future-proof solutions, making it an excellent platform for architects who want to maximize developer productivity while ensuring technical resilience and readiness for the next generation of .NET workloads.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/dotnet10rc1-is-knocking-at-the-door/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET 10 RC 1 is a strong signal of where the platform is heading. With architectural and performance improvements, a modernized C# 14, and production-ready containerization support, RC 1 is the ideal launchpad for planning your next step. While this is not yet the final LTS release, it’s fully supported for \u0026ldquo;go-live\u0026rdquo; production workloads and gives you the opportunity to validate your apps and infrastructure ahead of time. Leverage the new features and performance enhancements in your pilot projects now, and your transition to .NET 10 GA will be smoother than ever.\u003c/p\u003e\n\u003cp\u003eIn the next few articles, we’ll dive deeper into specific areas of interest, including advanced C# 14 features, best practices for leveraging .NET 10 RC 1 in cloud-native applications, and strategies for optimizing performance in high-load scenarios. Stay tuned!\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2025-09-10T09:00:00+02:00","id":"https://daily-devops.net/posts/dotnet10rc1-is-knocking-at-the-door/","language":"en","summary":".NET 10 RC 1 brings C# 14 features, major performance improvements, and architectural changes. Explore containerization, NativeAOT, and breaking changes.","tags":["csharp","dotnet","architecture"],"title":".NET 10 RC 1: Architectural Impact and C# 14","url":"https://daily-devops.net/posts/dotnet10rc1-is-knocking-at-the-door/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eLet’s skip the typical release-cycle enthusiasm for a second: Most IDE updates come and go. New features, some refactoring helpers, a bit of polish, then back to business as usual. \u003cstrong\u003eVisual Studio 2026 is different.\u003c/strong\u003e For once, the promise of \u0026ldquo;AI-native\u0026rdquo; isn’t just marketing. If Microsoft lands even half of what they’re previewing, it’s going to matter—a lot.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-road-to-2026-why-this-release-actually-matters\"\u003e\u003ca href=\"/posts/visual-studio-2026/#the-road-to-2026-why-this-release-actually-matters\" title=\"The Road to 2026: Why This Release Actually Matters\"\u003eThe Road to 2026: Why This Release Actually Matters\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFor anyone who’s built real-world enterprise solutions with .NET, the pain points are obvious. Tooling that can’t keep up with architectural complexity. Boilerplate and manual migration work that never quite get automated. Refactoring that stalls out at the \u0026ldquo;Hello World\u0026rdquo; demo stage.\u003cbr\u003e\nWith Visual Studio 2026, something fundamental is shifting. Microsoft isn’t just layering AI on top of the same old workflows—they’re weaving it into the foundation.\u003c/p\u003e\n\u003cp\u003eEarly signals are strong. Microsoft engineers are already using VS 2026 daily. The pace of public previews is picking up, and the roadmap finally looks like it’s written for actual developers—not just for marketing slides.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBottom line:\u003c/strong\u003e This isn’t just about UI updates or another Copilot sidebar. This is Microsoft’s bid to make Visual Studio the default \u0026ldquo;AI-native\u0026rdquo; platform for .NET, C#, and enterprise-scale software.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAt the time of writing, Visual Studio 2026 was available to download as an Insider Preview. \u003ca href=\"https://visualstudio.microsoft.com/insiders/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003cstrong\u003eDownload it now!\u003c/strong\u003e\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"expectations-raising-the-bar-for-ai-native-development\"\u003e\u003ca href=\"/posts/visual-studio-2026/#expectations-raising-the-bar-for-ai-native-development\" title=\"Expectations: Raising the Bar for AI-Native Development\"\u003eExpectations: Raising the Bar for AI-Native Development\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf \u0026ldquo;AI-powered\u0026rdquo; means only better code completion, the industry has learned nothing. Here’s what I’m actually looking for—and what VS 2026 \u003cem\u003eneeds\u003c/em\u003e to deliver if it wants to earn its place.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"agent-modelet-ai-do-the-dirty-work\"\u003e\u003ca href=\"/posts/visual-studio-2026/#agent-modelet-ai-do-the-dirty-work\" title=\"Agent Mode—Let AI Do the Dirty Work\"\u003eAgent Mode—Let AI Do the Dirty Work\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEnough with shallow integrations. I want AI that anticipates what comes next—refactorings that cut across dozens of projects, solution-wide migrations, architectural suggestions, and test scaffolding. And it should do this \u003cem\u003ewithout\u003c/em\u003e needing me to hand-craft a prompt every time.\u003c/p\u003e\n\u003cp\u003eAgent Mode needs to move past the chatbot gimmick. Context isn’t just the open file—it’s the shape of the solution, dependency graphs, historical commit data, and even open PRs. If the agent only helps with toy demos, it’s just Copilot with a different skin.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"mcpai-should-fit-my-domain-not-the-other-way-around\"\u003e\u003ca href=\"/posts/visual-studio-2026/#mcpai-should-fit-my-domain-not-the-other-way-around\" title=\"MCP—AI Should Fit My Domain, Not the Other Way Around\"\u003eMCP—AI Should Fit My Domain, Not the Other Way Around\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eLet’s talk Model Context Protocol (MCP). Most teams don’t work like Redmond. Our codebases have quirks. Our architecture, our debt, our standards—all different. MCP must allow \u003cem\u003egenuine extensibility\u003c/em\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBring your own model (not just OpenAI or whatever’s trendy this year)\u003c/li\u003e\n\u003cli\u003eShare prompts and automation patterns inside the org\u003c/li\u003e\n\u003cli\u003eFine-tune context, so the AI actually learns what matters to us\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIf this turns out to be just another wrapper for GPT with a new logo, I’ll keep my skepticism. If it unlocks true domain adaptation, then we’re talking about something disruptive.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"copilot-grows-up\"\u003e\u003ca href=\"/posts/visual-studio-2026/#copilot-grows-up\" title=\"Copilot Grows Up\"\u003eCopilot Grows Up\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCopilot is fine for snippets and tests. But in the .NET enterprise world, I want an assistant that understands architecture, not just syntax. Real intelligence would be recognizing design patterns, surfacing tech debt hotspots, and proposing migration paths—not just stubbing out another service class.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"benefits-what-professional-developers-stand-to-gain\"\u003e\u003ca href=\"/posts/visual-studio-2026/#benefits-what-professional-developers-stand-to-gain\" title=\"Benefits: What Professional Developers Stand to Gain\"\u003eBenefits: What Professional Developers Stand to Gain\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere’s where the marketing fluff usually takes over. But there are \u003cem\u003ereal\u003c/em\u003e benefits on the table—if, and only if, the execution matches the ambition.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"productivity-that-actually-frees-up-headspace\"\u003e\u003ca href=\"/posts/visual-studio-2026/#productivity-that-actually-frees-up-headspace\" title=\"Productivity That Actually Frees Up Headspace\"\u003eProductivity That Actually Frees Up Headspace\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf VS 2026 does its job, developers should get hours back each week. The goal isn’t \u0026ldquo;faster typing\u0026rdquo;—it’s less cognitive drag. Automate the 80% of tasks that any good developer can do in their sleep, and let us focus on problems that require human judgment.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"contextual-guidance-not-more-noise\"\u003e\u003ca href=\"/posts/visual-studio-2026/#contextual-guidance-not-more-noise\" title=\"Contextual Guidance, Not More Noise\"\u003eContextual Guidance, Not More Noise\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eI’m not interested in more squiggly lines or generic code suggestions. I want real context:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhat’s changed in this branch?\u003c/li\u003e\n\u003cli\u003eHow did the last migration go?\u003c/li\u003e\n\u003cli\u003eWhat are the unwritten architectural boundaries here?\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAI in VS 2026 should help me \u003cem\u003eunderstand\u003c/em\u003e and \u003cem\u003eenforce\u003c/em\u003e these realities—not distract me with irrelevant tips.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"quality-and-reliability-as-defaults\"\u003e\u003ca href=\"/posts/visual-studio-2026/#quality-and-reliability-as-defaults\" title=\"Quality and Reliability as Defaults\"\u003eQuality and Reliability as Defaults\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eTesting, static analysis, security scanning—these shouldn’t be optional. I expect VS 2026 to surface problems (and solutions) in real time, with recommendations that respect my architecture, not just industry best practices.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"customization-that-goes-deep\"\u003e\u003ca href=\"/posts/visual-studio-2026/#customization-that-goes-deep\" title=\"Customization That Goes Deep\"\u003eCustomization That Goes Deep\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eExtensions, themes, code policies—they matter. But I want even more:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eProject- and repo-level themes\u003c/li\u003e\n\u003cli\u003eAutomated enforcement of architectural decisions\u003c/li\u003e\n\u003cli\u003eAI-augmented linters and code analyzers that can be tuned for \u003cem\u003emy\u003c/em\u003e needs\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"architectural-impact-preparing-for-the-shift\"\u003e\u003ca href=\"/posts/visual-studio-2026/#architectural-impact-preparing-for-the-shift\" title=\"Architectural Impact: Preparing for the Shift\"\u003eArchitectural Impact: Preparing for the Shift\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is where most \u0026ldquo;future of IDE\u0026rdquo; articles check out. But if you’re building or maintaining real .NET systems, the architectural implications are front and center.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"automation-as-a-design-principle\"\u003e\u003ca href=\"/posts/visual-studio-2026/#automation-as-a-design-principle\" title=\"Automation as a Design Principle\"\u003eAutomation as a Design Principle\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAI isn’t a magic wand, but it’s an accelerant. Teams that design for automatable work—clear boundaries, documented contracts, metadata-rich solutions—will benefit most. Legacy codebases will need to evolve if they want to tap into these gains.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"governance-moves-upstream\"\u003e\u003ca href=\"/posts/visual-studio-2026/#governance-moves-upstream\" title=\"Governance Moves Upstream\"\u003eGovernance Moves Upstream\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eEnterprise devs know the pain: compliance, traceability, code review audits. With AI making changes, \u003cem\u003ewho owns what\u003c/em\u003e gets murkier. VS 2026 needs hooks for code provenance, audit trails, and review flows that can incorporate AI-generated changes as first-class citizens.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"devops-rewired\"\u003e\u003ca href=\"/posts/visual-studio-2026/#devops-rewired\" title=\"DevOps Rewired\"\u003eDevOps Rewired\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe lines between dev and ops will blur even more. Smarter CI/CD, dynamic builds, automated rollback logic—if VS 2026 integrates natively, we could finally see an end to brittle build scripts and manual deployment pain. Technical debt, at last, might become something you measure (and tackle) continuously.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-take\"\u003e\u003ca href=\"/posts/visual-studio-2026/#final-take\" title=\"Final Take\"\u003eFinal Take\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI’m not interested in hype. I’m interested in whether Visual Studio 2026 can move the needle for real developers building real systems.\u003cbr\u003e\nIf Microsoft follows through, this will reset expectations—not just for IDEs, but for what \u0026ldquo;intelligent tooling\u0026rdquo; really means. If they fall short, it’ll be just another missed opportunity.\u003c/p\u003e\n\u003cp\u003eEither way, enterprise .NET teams can’t afford to ignore what’s coming. Start prepping your codebase, your architecture, and your DevOps pipelines. The next era of software development is about to land in your IDE—ready or not.\n\u003cem\u003eWhat are your thoughts on Visual Studio 2026? Are you excited about the AI-native features, or skeptical about their real-world impact? Share your views in the comments below!\u003c/em\u003e\u003c/p\u003e","date_modified":"2026-02-16T08:03:33+01:00","date_published":"2025-09-10T01:00:00+02:00","id":"https://daily-devops.net/posts/visual-studio-2026/","language":"en","summary":"Visual Studio 2026 brings AI-native tooling. Learn what professional .NET teams should expect and how enterprise architecture adapts to changes.","tags":["visualstudio","csharp","dotnet"],"title":"Visual Studio 2026 - Why AI-Native Tooling Will Matter","url":"https://daily-devops.net/posts/visual-studio-2026/"},{"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\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\u003eIn C#, the \u003ccode\u003eStringValues\u003c/code\u003e struct belongs to the \u003ccode\u003eMicrosoft.Extensions.Primitives\u003c/code\u003e namespace, which is widely used in modern .NET applications. This struct plays a crucial role in efficiently managing string collections, especially when handling efficiently, particularly in contexts where multiple strings are involved. In this blog post, we’ll explore the purpose, usage, and key features of the \u003ccode\u003eStringValues\u003c/code\u003e struct in C#.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-is-the-stringvalues-struct\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#what-is-the-stringvalues-struct\" title=\"What is the StringValues struct?\"\u003eWhat is the \u003ccode\u003eStringValues\u003c/code\u003e struct?\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eStringValues\u003c/code\u003e struct is designed to hold one or more strings in a way that is optimized for performance in scenarios where a string might be passed as a single value or as a collection of values. Unlike other collections such as arrays or lists, \u003ccode\u003eStringValues\u003c/code\u003e is designed to offer a lightweight and efficient solution for managing strings that may vary in number, especially when it’s important to reduce memory allocations.\u003c/p\u003e\n\u003cp\u003eThis struct was introduced to cater to scenarios involving HTTP headers, query strings, and form values, where multiple values for the same key are common. For instance, when you query an HTTP endpoint that can return multiple values for a single key, \u003ccode\u003eStringValues\u003c/code\u003e allows the application to work with them in a simple, type-safe manner.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"key-characteristics-of-stringvalues\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#key-characteristics-of-stringvalues\" title=\"Key Characteristics of StringValues\"\u003eKey Characteristics of \u003ccode\u003eStringValues\u003c/code\u003e\u003c/a\u003e\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eSingle or Multiple Strings\u003c/strong\u003e:\nA \u003ccode\u003eStringValues\u003c/code\u003e object can represent either a single string or an array of strings. This allows developers to handle cases where a string value is expected but there may also be instances where multiple string values are present under the same key.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePerformance\u003c/strong\u003e:\nOne of the major benefits of the \u003ccode\u003eStringValues\u003c/code\u003e struct is its focus on performance. By internally managing strings with minimal allocations, \u003ccode\u003eStringValues\u003c/code\u003e helps reduce memory overhead, especially when dealing with multiple string values that could potentially involve lots of garbage collection in a high-performance application.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eValue Semantics\u003c/strong\u003e:\n\u003ccode\u003eStringValues\u003c/code\u003e can be implicitly converted to a \u003ccode\u003estring[]\u003c/code\u003e or a single \u003ccode\u003estring\u003c/code\u003e. It also supports indexing and enumeration, making it easy to work with the contained values, whether there is just one string or multiple.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\n\n\n\n\u003ch2 id=\"basic-usage-of-stringvalues\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#basic-usage-of-stringvalues\" title=\"Basic Usage of StringValues\"\u003eBasic Usage of \u003ccode\u003eStringValues\u003c/code\u003e\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTo better understand how the \u003ccode\u003eStringValues\u003c/code\u003e struct works, let\u0026rsquo;s take a look at some basic examples.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"single-string-value\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#single-string-value\" title=\"Single String Value\"\u003eSingle String Value\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIf you are dealing with a case where you expect only a single string, you can use \u003ccode\u003eStringValues\u003c/code\u003e 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\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.Extensions.Primitives\u003c/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\"\u003eStringValues\u003c/span\u003e \u003cspan class=\"n\"\u003evalues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringValues\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Hello, World!\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// Access the string directly\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\"\u003evalues\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Output: Hello, World!\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Access the string via array index\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\"\u003evalues\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// Output: Hello, World!\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn this example, we create a \u003ccode\u003eStringValues\u003c/code\u003e instance with a single string \u003ccode\u003e\u0026quot;Hello, World!\u0026quot;\u003c/code\u003e. The \u003ccode\u003eStringValues\u003c/code\u003e struct allows us to easily access the string as if it were a regular string, but the underlying type is still flexible enough to accommodate other scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"multiple-string-values\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#multiple-string-values\" title=\"Multiple String Values\"\u003eMultiple String Values\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn cases where multiple strings might be associated with a single key, such as in query parameters or HTTP headers, \u003ccode\u003eStringValues\u003c/code\u003e handles this gracefully. Here\u0026rsquo;s an example of how you would initialize a \u003ccode\u003eStringValues\u003c/code\u003e with multiple strings:\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.Primitives\u003c/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\"\u003eStringValues\u003c/span\u003e \u003cspan class=\"n\"\u003evalues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringValues\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;First Value\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Second Value\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Third Value\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// Access individual values\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\"\u003evalues\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// Output: First Value\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\"\u003evalues\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// Output: Second Value\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\"\u003evalues\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// Output: Third Value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan 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// Iterate through all values\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\"\u003evalues\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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=\"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\u003eIn this case, \u003ccode\u003eStringValues\u003c/code\u003e holds an array of strings. You can access the strings using indexing and can also enumerate through all the contained values with a simple \u003ccode\u003eforeach\u003c/code\u003e loop.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"implicit-conversion-to-string\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#implicit-conversion-to-string\" title=\"Implicit Conversion to string[]\"\u003eImplicit Conversion to \u003ccode\u003estring[]\u003c/code\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eYou can also implicitly convert \u003ccode\u003eStringValues\u003c/code\u003e to an array of strings. This makes it easy to interact with the object in contexts where an array is expected, such as when passing the value to methods that require a string array.\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.Primitives\u003c/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\"\u003eStringValues\u003c/span\u003e \u003cspan class=\"n\"\u003evalues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringValues\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Apple\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Banana\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Cherry\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// Implicit conversion to string[]\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\"\u003estringArray\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003evalues\u003c/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// Print out the values\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\"\u003estringArray\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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=\"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\u003eThis ability to implicitly convert to a \u003ccode\u003estring[]\u003c/code\u003e makes the \u003ccode\u003eStringValues\u003c/code\u003e struct highly compatible with other parts of the .NET ecosystem.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"advanced-features-of-stringvalues\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#advanced-features-of-stringvalues\" title=\"Advanced Features of StringValues\"\u003eAdvanced Features of \u003ccode\u003eStringValues\u003c/code\u003e\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eApart from the basic functionality, the \u003ccode\u003eStringValues\u003c/code\u003e struct has a few more advanced features that can be useful in certain scenarios.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"checking-for-empty-or-null-values\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#checking-for-empty-or-null-values\" title=\"Checking for Empty or Null Values\"\u003eChecking for Empty or Null Values\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe \u003ccode\u003eStringValues\u003c/code\u003e struct provides a way to check whether it contains any values, or if it’s essentially empty. You can use the \u003ccode\u003eIsNullOrEmpty\u003c/code\u003e property to make such checks:\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.Primitives\u003c/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\"\u003eStringValues\u003c/span\u003e \u003cspan class=\"n\"\u003evalues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringValues\u003c/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\"\u003eStringValues\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\"\u003evalues\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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;No values present\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// Adding a value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003evalues\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringValues\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Hello, World!\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\"\u003eStringValues\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\"\u003evalues\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\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;There is a value present\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\n\n\n\n\u003ch3 id=\"stringvalues-as-a-dictionary-key\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#stringvalues-as-a-dictionary-key\" title=\"StringValues as a Dictionary Key\"\u003eStringValues as a Dictionary Key\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eStringValues\u003c/code\u003e is also designed to work as a key in dictionaries. This can be especially useful when you are dealing with collections of query parameters or HTTP headers in web applications, where the same key might have multiple associated 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=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.Extensions.Primitives\u003c/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\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eStringValues\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003edictionary\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=\"n\"\u003eStringValues\u003c/span\u003e\u003cspan class=\"p\"\u003e,\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=\"n\"\u003eStringValues\u003c/span\u003e \u003cspan class=\"n\"\u003ekey\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eStringValues\u003c/span\u003e\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\"\u003edictionary\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=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Some value associated with multiple keys\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\"\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\"\u003edictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\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\u003eIn this example, \u003ccode\u003eStringValues\u003c/code\u003e can be used as a dictionary key, allowing the association of a string value with one or more key values.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-to-use-stringvalues\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#when-to-use-stringvalues\" title=\"When to Use StringValues\"\u003eWhen to Use \u003ccode\u003eStringValues\u003c/code\u003e\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eStringValues\u003c/code\u003e struct shines in scenarios where you expect multiple values for a single key, such as:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHTTP Headers\u003c/strong\u003e: Web applications often need to manage headers with multiple values. For instance, a header like \u003ccode\u003eAccept-Language\u003c/code\u003e could contain multiple languages.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eQuery Parameters\u003c/strong\u003e: When dealing with query strings, a single parameter might appear multiple times with different values.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eForm Data\u003c/strong\u003e: Similar to query parameters, form fields can also contain multiple values for the same key.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn such scenarios, \u003ccode\u003eStringValues\u003c/code\u003e provides an elegant and efficient way to manage string collections without the need for more complex structures like lists or arrays.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/understanding-csharp-stringvalues/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eStringValues\u003c/code\u003e struct in C# is an essential tool for developers working with scenarios where one or more strings need to be handled efficiently. By supporting both single and multiple values, being lightweight and optimized for performance, and offering easy integration with other .NET components, it simplifies the management of string collections, particularly in web applications and services.\u003c/p\u003e\n\u003cp\u003eWhether you\u0026rsquo;re dealing with HTTP headers, query parameters, or form data, understanding and leveraging the \u003ccode\u003eStringValues\u003c/code\u003e struct can help you write cleaner, more efficient code. By utilizing its features, developers can manage string collections more effectively, reducing memory overhead and increasing the performance of their applications.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2024-12-30T16:00:00+01:00","id":"https://daily-devops.net/posts/understanding-csharp-stringvalues/","language":"en","summary":"Learn about C# StringValues type, its features, usage patterns, and performance benefits in efficiently handling string values in modern .NET applications.","tags":["csharp","dotnet","hidden-gems"],"title":"Understanding the C# `StringValues`: A Comprehensive Guide","url":"https://daily-devops.net/posts/understanding-csharp-stringvalues/"},{"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\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/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the ever-evolving world of .NET development, managing project configurations effectively is crucial for maintaining a clean and efficient build process. One of the less frequently discussed but highly useful properties is \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e. This property, when correctly utilized, can streamline your build process and ensure that your project is configured properly depending on the build environment. In this article, we\u0026rsquo;ll explore the \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property with concrete examples and discuss best practices for using it effectively.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"understanding-the-buildinginsidevisualstudio-property\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#understanding-the-buildinginsidevisualstudio-property\" title=\"Understanding the BuildingInsideVisualStudio Property\"\u003eUnderstanding the \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e Property\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property is a conditional flag that can be used within your project files (.csproj) to apply certain settings or include/exclude packages and references based on whether the project is being built inside Visual Studio. This property is particularly useful when you need to differentiate between builds triggered from Visual Studio and those triggered from other environments such as command-line builds or CI/CD pipelines. See also \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMSBuild conditions\u003c/a\u003e and \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-properties\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003ecommon project properties\u003c/a\u003e for context.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"example-adding-a-package-reference-conditionally\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#example-adding-a-package-reference-conditionally\" title=\"Example: Adding a Package Reference Conditionally\"\u003eExample: Adding a Package Reference Conditionally\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLet\u0026rsquo;s start with a practical example: adding a package reference only when the project is being built inside Visual Studio. This can be useful when you want to include certain tools or analyzers only in the development environment to keep the build lean for production.\u003c/p\u003e\n\u003cp\u003eAssuming you want to add a reference to \u003ccode\u003eSonarAnalyzer.CSharp\u003c/code\u003e, a popular static code analysis tool, but only when building the project within Visual Studio, you can use the \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property to conditionally include this package reference in your \u003ccode\u003e.csproj\u003c/code\u003e file. Why would you want to do this? It\u0026rsquo;s already included in your CI/CD pipeline, so you don\u0026rsquo;t need it in your local development environment? The answer is simple: you want to have the same code analysis rules and hints in your local development environment as in your CI/CD pipeline. This way, you can fix issues early and avoid surprises when pushing your code to the repository, and executing maybe long-running CI/CD pipelines.\u003c/p\u003e\n\u003cp\u003eHere\u0026rsquo;s how you can do 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=\"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\n\u003c/span\u003e\u003c/span\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;$(BuildingInsideVisualStudio)\u0026#39; == \u0026#39;true\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;SonarAnalyzer.CSharp\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;9.6.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\u003cspan 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;!-- Rest of the project file --\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;/Project\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=\"beware-of-pitfalls-with-buildinginsidevisualstudio\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#beware-of-pitfalls-with-buildinginsidevisualstudio\" title=\"Beware of Pitfalls with BuildingInsideVisualStudio\"\u003eBeware of Pitfalls with \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eWhile the \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property is a powerful tool for customizing your build process, there are some common pitfalls to be aware of when using it in your project files. Let\u0026rsquo;s explore these pitfalls and how to avoid them.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"expectation-that-buildinginsidevisualstudio-is-configured-correctly\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#expectation-that-buildinginsidevisualstudio-is-configured-correctly\" title=\"Expectation that BuildingInsideVisualStudio is configured correctly\"\u003eExpectation that \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e is configured correctly\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eWhen using the \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property, it\u0026rsquo;s important to remember that it may not always be set to \u003ccode\u003etrue\u003c/code\u003e. For example, when building the project outside of Visual Studio, this property may be empty or set to a different value. Relying on the assumption that \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e will always be \u003ccode\u003etrue\u003c/code\u003e or \u003ccode\u003efalse\u003c/code\u003e can lead to unexpected behavior and misconfigurations.\u003c/p\u003e\n\u003cp\u003eWhen using conditional checks in your project files, it\u0026rsquo;s essential to ensure that the property values are correctly set and evaluated. Misconfiguring the property values can lead to unexpected behavior or missing configurations. In the case of \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e, the only valid fact is \u003ccode\u003etrue\u003c/code\u003e when the project is built inside Visual Studio. Otherwise, it could be \u003ccode\u003efalse\u003c/code\u003e or empty.\u003c/p\u003e\n\u003cp\u003eTo avoid issues where the property might be empty or not set, you should use a condition that checks against \u003ccode\u003etrue\u003c/code\u003e explicitly. This ensures that the feature is enabled only when the property is explicitly set to \u003ccode\u003etrue\u003c/code\u003e. Here\u0026rsquo;s an example of a conditional property check:\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\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"c\"\u003e\u0026lt;!-- Instead of this 👎 --\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\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(BuildingInsideVisualStudio)\u0026#39; == \u0026#39;false\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;FeatureXPackage\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1.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\u003cspan 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;!-- Use this 👍 --\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\u003c/span\u003e \u003cspan class=\"na\"\u003eCondition=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#39;$(BuildingInsideVisualStudio)\u0026#39; != \u0026#39;true\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;FeatureXPackage\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eVersion=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1.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\u003cspan 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;/Project\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch4 id=\"benefits\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#benefits\" title=\"Benefits\"\u003eBenefits\u003c/a\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eAvoids Null or Empty Values:\u003c/strong\u003e With a condition like \u003ccode\u003e!= 'true'\u003c/code\u003e, you ensure that the feature is enabled only when the property is not explicitly set to \u003ccode\u003etrue\u003c/code\u003e, avoiding issues with null or empty values.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConsistent Behavior:\u003c/strong\u003e This approach ensures consistent behavior across different environments and build scenarios.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch4 id=\"potential-errors\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#potential-errors\" title=\"Potential Errors\"\u003ePotential Errors\u003c/a\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMisconfigured Conditions:\u003c/strong\u003e Incorrectly configured conditions can lead to unexpected behavior or missing configurations. Always test your project settings thoroughly.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUnintended Enabling:\u003c/strong\u003e Be cautious of unintended enabling of features or dependencies if the property is not set as expected.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch3 id=\"assumption-that-this-works-in-other-ides\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#assumption-that-this-works-in-other-ides\" title=\"Assumption that this works in other IDEs\"\u003eAssumption that this works in other IDEs\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eIn general assumptions are bad. The \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property is a Visual Studio specific property. It is not guaranteed to work in other IDEs like Visual Studio Code, or any other IDE. If you want to have the same behavior in other IDEs, you have to check if the IDE supports this property or if there is a similar property available. Like in JetBrains Rider, you can use the \u003ccode\u003eBuildingByReSharper\u003c/code\u003e property.\u003c/p\u003e\n\n\n\n\n\u003ch4 id=\"potential-errors-1\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#potential-errors-1\" title=\"Potential Errors\"\u003ePotential Errors\u003c/a\u003e\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eIncompatibility with Other IDEs:\u003c/strong\u003e Relying solely on \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e can lead to incompatibility issues when using other IDEs or build environments. Always check the compatibility of the property with your target environment.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/buildinginsidevisualstudio/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eBuildingInsideVisualStudio\u003c/code\u003e property and conditional checks in your .NET project files offer powerful ways to customize your build process depending on the environment. By following best practices such as checking conditions against \u003ccode\u003etrue\u003c/code\u003e and being mindful of how properties are used, you can avoid common pitfalls and optimize your build configurations for different scenarios.\u003c/p\u003e\n\u003cp\u003eLeveraging these techniques not only helps in maintaining a clean and efficient build process but also ensures that your project is configured correctly across various development environments. As always, thorough testing and careful configuration are key to making the most out of these features.\u003c/p\u003e\n\u003cp\u003eFeel free to incorporate these examples into your own projects and see how they can simplify and improve your build process. Happy coding!\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2024-09-10T17:00:00+02:00","id":"https://daily-devops.net/posts/buildinginsidevisualstudio/","language":"en","summary":"Learn how to use the BuildingInsideVisualStudio property in .NET to conditionally include packages, optimize builds, and streamline developer workflows.","tags":["msbuild","visualstudio","bestpractices","csharp","dotnet","hidden-gems"],"title":"BuildingInsideVisualStudio: .NET Project Properties","url":"https://daily-devops.net/posts/buildinginsidevisualstudio/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eFor over 12 years, NuGet package management has been part of the .NET ecosystem with direct integrations to various IDEs, CLIs and build systems. But a feature took 12 years before it appeared and certainly needs some more maintenance until it is mature!\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-issue\"\u003e\u003ca href=\"/posts/manage-nuget-packages-centrally/#the-issue\" title=\"The issue\"\u003eThe issue\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eRegardless of the code version management strategy, mono-repository vs. poly-repository, there has always been a need to synchronize the individual projects in the versions of NuGet packages used. Reasons for this are compatibility and security, but also new functionalities or bug fixes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"earlier-approaches\"\u003e\u003ca href=\"/posts/manage-nuget-packages-centrally/#earlier-approaches\" title=\"Earlier approaches\"\u003eEarlier approaches\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOver the years, the requirements in this area have evolved more and more, so that the previous solution approaches increasingly reached their limits. Not only the uniform use of the same package version, but also the general use of a package in all related projects of a solution was taken up and developed further in this context. However, the main shortcoming could never be solved; until now, manual intervention by a developer was always necessary to update the version of the packages used. The existing integrations of IDEs and CLIs produced more errors than they could fix.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"central-package-management-cpm\"\u003e\u003ca href=\"/posts/manage-nuget-packages-centrally/#central-package-management-cpm\" title=\"Central Package Management (CPM)\"\u003eCentral Package Management (CPM)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNow the request has been fulfilled and in April 2022 the \u003ca href=\"https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eCentral Package Management (\u0026ldquo;CPM\u0026rdquo;)\u003c/a\u003e was introduced and released along with NuGet version 6.2 and some complementary features.\u003c/p\u003e\n\u003cp\u003eTo enable central package management, the MSBuild property \u003ccode\u003eManagePackageVersionsCentrally\u003c/code\u003e is set to \u003ccode\u003etrue\u003c/code\u003e in the \u003ccode\u003eDirectory.Packages.props\u003c/code\u003e file.\u003c/p\u003e\n\u003cp\u003eFor version listing and management, \u003ccode\u003ePackageVersion\u003c/code\u003e elements are required, each containing the package name and the version to be used. The next step is to remove the \u003ccode\u003eVersion\u003c/code\u003e attribute from all \u003ccode\u003ePackageReference\u003c/code\u003e elements in the project files. This migrates the solution and it will use the central package management from now on.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"additional-feature-transitive-pinning\"\u003e\u003ca href=\"/posts/manage-nuget-packages-centrally/#additional-feature-transitive-pinning\" title=\"Additional feature: Transitive pinning\"\u003eAdditional feature: Transitive pinning\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eSetting the MSBuild property \u003ccode\u003eCentralPackageTransitivePinningEnabled\u003c/code\u003e to \u003ccode\u003etrue\u003c/code\u003e tells NuGet to update all transitive dependencies from their explicitly defined dependencies. This property can be set in both \u003ca href=\"https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022#directorybuildprops-and-directorybuildtargets\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eDirectory.Build.props\u003c/code\u003e\u003c/a\u003e and the aforementioned \u003ccode\u003eDirectory.Packages.props\u003c/code\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"additional-feature-global-package-references\"\u003e\u003ca href=\"/posts/manage-nuget-packages-centrally/#additional-feature-global-package-references\" title=\"Additional feature: Global Package References\"\u003eAdditional feature: Global Package References\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eAnother feature is \u003ccode\u003eGlobalPackageReference\u003c/code\u003e, which can be used to reference a package in any project of the solution / repository, such as code analyzer. This kind of package referencing should also be done in \u003ccode\u003eDirectory.Packages.props\u003c/code\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"summary\"\u003e\u003ca href=\"/posts/manage-nuget-packages-centrally/#summary\" title=\"Summary\"\u003eSummary\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAll in all, a great enhancement to the NuGet system. However, there are currently some issues with the Visual Studio or .NET CLI integration.\u003c/p\u003e\n\u003cp\u003eBoth integrations are able to evaluate the package references and recover the packages. However, when updating with Visual Studio, the XML structure of the project is updated incorrectly, so manual rework is required.\u003c/p\u003e\n\u003cp\u003eWhen the .NET CLI wants to add a reference to a project, CPM is ignored and build errors occur again.\u003c/p\u003e\n\u003cp\u003eHowever, this should not deter you, because existing integrations such as \u003ca href=\"https://github.com/dependabot\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eGitHubs Dependabot\u003c/a\u003e provide excellent results.\u003c/p\u003e","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2023-04-17T08:30:00+02:00","id":"https://daily-devops.net/posts/manage-nuget-packages-centrally/","language":"en","summary":"Learn how to centrally manage NuGet packages in .NET solutions using Directory.Packages.props for better dependency management and version control.","tags":["nuget","bestpractices","csharp","dependency-management","dotnet","hidden-gems","technicaldebt"],"title":"Manage NuGet Packages Centrally","url":"https://daily-devops.net/posts/manage-nuget-packages-centrally/"}],"language":"en","title":"C# Programming Language Articles on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}